From 37bf6965d670a301848a7db45359f760531b7261 Mon Sep 17 00:00:00 2001 From: "r.zarchi" Date: Mon, 11 Apr 2022 11:09:29 +0430 Subject: [PATCH 01/75] Disable messageview_show_next item when messageview_return_to_list is checked --- app/ui/legacy/src/main/res/xml/general_settings.xml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/ui/legacy/src/main/res/xml/general_settings.xml b/app/ui/legacy/src/main/res/xml/general_settings.xml index 632383a834..5f089831da 100644 --- a/app/ui/legacy/src/main/res/xml/general_settings.xml +++ b/app/ui/legacy/src/main/res/xml/general_settings.xml @@ -366,11 +366,13 @@ android:title="@string/volume_navigation_title" /> -- GitLab From 9b9b3d8f6949b22ccecab8f008b9e6d9d05bc0cc Mon Sep 17 00:00:00 2001 From: cketti Date: Mon, 11 Apr 2022 15:09:42 +0200 Subject: [PATCH 02/75] Prepare for version 6.100 --- app/k9mail/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/k9mail/build.gradle b/app/k9mail/build.gradle index fe663f1611..aae39b45b1 100644 --- a/app/k9mail/build.gradle +++ b/app/k9mail/build.gradle @@ -47,8 +47,8 @@ android { applicationId "com.fsck.k9" testApplicationId "com.fsck.k9.tests" - versionCode 29015 - versionName '5.915' + versionCode 31000 + versionName '6.100-SNAPSHOT' // Keep in sync with the resource string array 'supported_languages' resConfigs "in", "br", "ca", "cs", "cy", "da", "de", "et", "en", "en_GB", "es", "eo", "eu", "fr", "gd", "gl", -- GitLab From 0dbca2dddfcc4717554904679bfea0f48bb43085 Mon Sep 17 00:00:00 2001 From: Alexandre Date: Wed, 6 Apr 2022 00:12:06 +0200 Subject: [PATCH 03/75] =?UTF-8?q?Append=20a=20space=20to=20=C2=BB=20and=20?= =?UTF-8?q?=E2=80=BA=20in=20messages=20list.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/main/java/com/fsck/k9/fragment/MessageListAdapter.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/ui/legacy/src/main/java/com/fsck/k9/fragment/MessageListAdapter.kt b/app/ui/legacy/src/main/java/com/fsck/k9/fragment/MessageListAdapter.kt index d6e017e9ee..468ac24c52 100644 --- a/app/ui/legacy/src/main/java/com/fsck/k9/fragment/MessageListAdapter.kt +++ b/app/ui/legacy/src/main/java/com/fsck/k9/fragment/MessageListAdapter.kt @@ -83,8 +83,8 @@ class MessageListAdapter internal constructor( } private fun recipientSigil(toMe: Boolean, ccMe: Boolean) = when { - toMe -> res.getString(R.string.messagelist_sent_to_me_sigil) - ccMe -> res.getString(R.string.messagelist_sent_cc_me_sigil) + toMe -> res.getString(R.string.messagelist_sent_to_me_sigil) + " " + ccMe -> res.getString(R.string.messagelist_sent_cc_me_sigil) + " " else -> "" } -- GitLab From a7562fe3f367f9c92ca8aa79fbad3aa3ed99d97c Mon Sep 17 00:00:00 2001 From: cketti Date: Tue, 22 Mar 2022 05:53:50 +0100 Subject: [PATCH 04/75] Add a new parser for SMTP responses --- mail/protocols/smtp/build.gradle | 2 + .../mail/transport/smtp/SmtpHelloResponse.kt | 8 + .../fsck/k9/mail/transport/smtp/SmtpLogger.kt | 9 + .../k9/mail/transport/smtp/SmtpResponse.kt | 62 ++ .../mail/transport/smtp/SmtpResponseParser.kt | 411 +++++++++++ .../smtp/SmtpResponseParserException.kt | 3 + .../mail/transport/smtp/StatusCodeClass.java | 2 +- .../transport/smtp/SmtpResponseParserTest.kt | 675 ++++++++++++++++++ .../mail/transport/smtp/SmtpResponseTest.kt | 172 +++++ .../k9/mail/transport/smtp/TestSmtpLogger.kt | 15 + 10 files changed, 1358 insertions(+), 1 deletion(-) create mode 100644 mail/protocols/smtp/src/main/java/com/fsck/k9/mail/transport/smtp/SmtpHelloResponse.kt create mode 100644 mail/protocols/smtp/src/main/java/com/fsck/k9/mail/transport/smtp/SmtpLogger.kt create mode 100644 mail/protocols/smtp/src/main/java/com/fsck/k9/mail/transport/smtp/SmtpResponse.kt create mode 100644 mail/protocols/smtp/src/main/java/com/fsck/k9/mail/transport/smtp/SmtpResponseParser.kt create mode 100644 mail/protocols/smtp/src/main/java/com/fsck/k9/mail/transport/smtp/SmtpResponseParserException.kt create mode 100644 mail/protocols/smtp/src/test/java/com/fsck/k9/mail/transport/smtp/SmtpResponseParserTest.kt create mode 100644 mail/protocols/smtp/src/test/java/com/fsck/k9/mail/transport/smtp/SmtpResponseTest.kt create mode 100644 mail/protocols/smtp/src/test/java/com/fsck/k9/mail/transport/smtp/TestSmtpLogger.kt diff --git a/mail/protocols/smtp/build.gradle b/mail/protocols/smtp/build.gradle index d1210c3a8d..6707ad5f3c 100644 --- a/mail/protocols/smtp/build.gradle +++ b/mail/protocols/smtp/build.gradle @@ -1,4 +1,5 @@ apply plugin: 'com.android.library' +apply plugin: 'org.jetbrains.kotlin.android' if (rootProject.testCoverage) { apply plugin: 'jacoco' @@ -8,6 +9,7 @@ dependencies { api project(":mail:common") implementation "commons-io:commons-io:${versions.commonsIo}" + implementation "com.squareup.okio:okio:${versions.okio}" implementation "com.jakewharton.timber:timber:${versions.timber}" implementation "androidx.annotation:annotation:${versions.androidxAnnotation}" diff --git a/mail/protocols/smtp/src/main/java/com/fsck/k9/mail/transport/smtp/SmtpHelloResponse.kt b/mail/protocols/smtp/src/main/java/com/fsck/k9/mail/transport/smtp/SmtpHelloResponse.kt new file mode 100644 index 0000000000..559acdbaae --- /dev/null +++ b/mail/protocols/smtp/src/main/java/com/fsck/k9/mail/transport/smtp/SmtpHelloResponse.kt @@ -0,0 +1,8 @@ +package com.fsck.k9.mail.transport.smtp + +internal sealed interface SmtpHelloResponse { + val response: SmtpResponse + + data class Error(override val response: SmtpResponse) : SmtpHelloResponse + data class Hello(override val response: SmtpResponse, val keywords: Map>) : SmtpHelloResponse +} diff --git a/mail/protocols/smtp/src/main/java/com/fsck/k9/mail/transport/smtp/SmtpLogger.kt b/mail/protocols/smtp/src/main/java/com/fsck/k9/mail/transport/smtp/SmtpLogger.kt new file mode 100644 index 0000000000..b442c49da9 --- /dev/null +++ b/mail/protocols/smtp/src/main/java/com/fsck/k9/mail/transport/smtp/SmtpLogger.kt @@ -0,0 +1,9 @@ +package com.fsck.k9.mail.transport.smtp + +interface SmtpLogger { + val isRawProtocolLoggingEnabled: Boolean + + fun log(message: String, vararg args: Any?) = log(throwable = null, message, *args) + + fun log(throwable: Throwable?, message: String, vararg args: Any?) +} diff --git a/mail/protocols/smtp/src/main/java/com/fsck/k9/mail/transport/smtp/SmtpResponse.kt b/mail/protocols/smtp/src/main/java/com/fsck/k9/mail/transport/smtp/SmtpResponse.kt new file mode 100644 index 0000000000..75bf0fadfb --- /dev/null +++ b/mail/protocols/smtp/src/main/java/com/fsck/k9/mail/transport/smtp/SmtpResponse.kt @@ -0,0 +1,62 @@ +package com.fsck.k9.mail.transport.smtp + +internal data class SmtpResponse( + val replyCode: Int, + val statusCode: StatusCode?, + val texts: List +) { + val isNegativeResponse = replyCode >= 400 + + fun toLogString(omitText: Boolean, linePrefix: String): String { + return buildString { + if (omitText) { + append(linePrefix) + append(replyCode) + appendIfNotNull(statusCode, prefix = ' ') + if (texts.isNotEmpty()) { + append(" [omitted]") + } + } else { + if (texts.size > 1) { + for (i in 0 until texts.lastIndex) { + append(linePrefix) + append(replyCode) + if (statusCode == null) { + append('-') + } else { + appendIfNotNull(statusCode, prefix = '-') + append(' ') + } + append(texts[i]) + appendLine() + } + } + + append(linePrefix) + append(replyCode) + appendIfNotNull(statusCode, prefix = ' ') + if (texts.isNotEmpty()) { + append(' ') + append(texts.last()) + } + } + } + } + + private fun StringBuilder.appendIfNotNull(statusCode: StatusCode?, prefix: Char) { + if (statusCode != null) { + append(prefix) + append(statusCode.statusClass.codeClass) + append('.') + append(statusCode.subject) + append('.') + append(statusCode.detail) + } + } +} + +internal data class StatusCode( + val statusClass: StatusCodeClass, + val subject: Int, + val detail: Int +) diff --git a/mail/protocols/smtp/src/main/java/com/fsck/k9/mail/transport/smtp/SmtpResponseParser.kt b/mail/protocols/smtp/src/main/java/com/fsck/k9/mail/transport/smtp/SmtpResponseParser.kt new file mode 100644 index 0000000000..f1ff671f96 --- /dev/null +++ b/mail/protocols/smtp/src/main/java/com/fsck/k9/mail/transport/smtp/SmtpResponseParser.kt @@ -0,0 +1,411 @@ +package com.fsck.k9.mail.transport.smtp + +import com.fsck.k9.mail.filter.PeekableInputStream +import okio.Buffer +import okio.BufferedSource + +private const val CR = '\r' +private const val LF = '\n' +private const val SPACE = ' ' +private const val DASH = '-' +private const val HTAB = '\t' +private const val DOT = '.' +private const val END_OF_STREAM = -1 + +/** + * Parser for SMTP response lines. + * + * Supports enhanced status codes as defined in RFC 2034. + * + * Unfortunately at least one popular implementation doesn't always send an enhanced status code even though its EHLO + * response contains the ENHANCEDSTATUSCODES keyword. Begrudgingly, we allow this and other minor standard violations. + * However, we output a log message when such a case is encountered. + */ +internal class SmtpResponseParser( + private val logger: SmtpLogger, + private val input: PeekableInputStream +) { + private val logBuffer = Buffer() + + fun readGreeting(): SmtpResponse { + // We're not interested in the domain or address literal in the greeting. So we use the standard parser. + return readResponse(enhancedStatusCodes = false) + } + + fun readHelloResponse(): SmtpHelloResponse { + val replyCode = readReplyCode() + + if (replyCode != 250) { + val response = readResponseAfterReplyCode(replyCode, enhancedStatusCodes = false) + return SmtpHelloResponse.Error(response) + } + + val texts = mutableListOf() + + // Read first line containing 'domain' and maybe 'ehlo-greet' (we don't check the syntax and allow any text) + when (val char = peekChar()) { + SPACE -> { + expect(SPACE) + + val text = readUntilEndOfLine().readUtf8() + + expect(CR) + expect(LF) + + return SmtpHelloResponse.Hello( + response = SmtpResponse(replyCode, statusCode = null, texts = listOf(text)), + keywords = emptyMap() + ) + } + DASH -> { + expect(DASH) + + val text = readUntilEndOfLine().readUtf8() + texts.add(text) + + expect(CR) + expect(LF) + } + else -> unexpectedCharacterError(char) + } + + val keywords = mutableMapOf>() + + // Read EHLO keywords and parameters + while (true) { + val currentReplyCode = readReplyCode() + if (currentReplyCode != replyCode) { + parserError("Multi-line response with reply codes not matching: $replyCode != $currentReplyCode") + } + + when (val char = peekChar()) { + SPACE -> { + expect(SPACE) + + val bufferedSource = readUntilEndOfLine() + val ehloLine = bufferedSource.readEhloLine() + texts.add(ehloLine) + + parseEhloLine(ehloLine, keywords) + + expect(CR) + expect(LF) + + return SmtpHelloResponse.Hello( + response = SmtpResponse(replyCode, statusCode = null, texts), + keywords = keywords + ) + } + DASH -> { + expect(DASH) + + val bufferedSource = readUntilEndOfLine() + val ehloLine = bufferedSource.readEhloLine() + texts.add(ehloLine) + + parseEhloLine(ehloLine, keywords) + + expect(CR) + expect(LF) + } + else -> unexpectedCharacterError(char) + } + } + } + + private fun parseEhloLine(ehloLine: String, keywords: MutableMap>) { + val parts = ehloLine.split(" ") + val keyword = checkAndNormalizeEhloKeyword(parts[0]) + val parameters = checkEhloParameters(parts) + + if (keywords.containsKey(keyword)) { + parserError("Same EHLO keyword present in more than one response line") + } + + keywords[keyword] = parameters + } + + private fun checkAndNormalizeEhloKeyword(text: String): String { + val keyword = text.uppercase() + if (!keyword[0].isCapitalAlphaDigit() || keyword.any { !it.isCapitalAlphaDigit() && it != DASH }) { + parserError("EHLO keyword contains invalid character") + } + + return keyword + } + + private fun checkEhloParameters(parts: List): List { + for (i in 1..parts.lastIndex) { + val parameter = parts[i] + if (parameter.isEmpty()) { + parserError("EHLO parameter must not be empty") + } else if (parameter.any { it.code !in 33..126 }) { + parserError("EHLO parameter contains invalid character") + } + } + + return parts.drop(1) + } + + fun readResponse(enhancedStatusCodes: Boolean): SmtpResponse { + logBuffer.clear() + + val replyCode = readReplyCode() + return readResponseAfterReplyCode(replyCode, enhancedStatusCodes) + } + + private fun readResponseAfterReplyCode(replyCode: Int, enhancedStatusCodes: Boolean): SmtpResponse { + val texts = mutableListOf() + var statusCode: StatusCode? = null + var isFirstLine = true + + fun BufferedSource.maybeReadAndCompareEnhancedStatusCode(replyCode: Int): StatusCode? { + val currentStatusCode = maybeReadEnhancedStatusCode(replyCode) + if (!isFirstLine && statusCode != currentStatusCode) { + parserError( + "Multi-line response with enhanced status codes not matching: " + + "$statusCode != $currentStatusCode" + ) + } + isFirstLine = false + + return currentStatusCode + } + + while (true) { + when (val char = peekChar()) { + CR -> { + expect(CR) + expect(LF) + + return SmtpResponse(replyCode, statusCode, texts) + } + SPACE -> { + expect(SPACE) + + val bufferedSource = readUntilEndOfLine() + + if (enhancedStatusCodes) { + statusCode = bufferedSource.maybeReadAndCompareEnhancedStatusCode(replyCode) + } + + val textString = bufferedSource.readTextString() + if (textString.isNotEmpty()) { + texts.add(textString) + } + + expect(CR) + expect(LF) + + return SmtpResponse(replyCode, statusCode, texts) + } + DASH -> { + expect(DASH) + + val bufferedSource = readUntilEndOfLine() + + if (enhancedStatusCodes) { + statusCode = bufferedSource.maybeReadAndCompareEnhancedStatusCode(replyCode) + } + + val textString = bufferedSource.readTextString() + texts.add(textString) + + expect(CR) + expect(LF) + + val currentReplyCode = readReplyCode() + if (currentReplyCode != replyCode) { + parserError( + "Multi-line response with reply codes not matching: $replyCode != $currentReplyCode" + ) + } + } + else -> unexpectedCharacterError(char) + } + } + } + + private fun readReplyCode(): Int { + return readReplyCode1() * 100 + readReplyCode2() * 10 + readReplyCode3() + } + + private fun readReplyCode1(): Int { + val replyCode1 = readDigit() + if (replyCode1 !in 2..5) parserError("Unsupported 1st reply code digit: $replyCode1") + + return replyCode1 + } + + private fun readReplyCode2(): Int { + val replyCode2 = readDigit() + if (replyCode2 !in 0..5) { + logger.log("2nd digit of reply code outside of specified range (0..5): %d", replyCode2) + } + + return replyCode2 + } + + private fun readReplyCode3(): Int { + return readDigit() + } + + private fun readDigit(): Int { + val char = readChar() + if (char !in '0'..'9') unexpectedCharacterError(char) + + return char - '0' + } + + private fun expect(expectedChar: Char) { + val char = readChar() + if (char != expectedChar) unexpectedCharacterError(char) + } + + private fun readByte(): Int { + return input.read() + .also { + throwIfEndOfStreamReached(it) + logBuffer.writeByte(it) + } + } + + private fun readChar(): Char { + return readByte().toChar() + } + + private fun peekChar(): Char { + return input.peek() + .also { throwIfEndOfStreamReached(it) } + .toChar() + } + + private fun throwIfEndOfStreamReached(data: Int) { + if (data == END_OF_STREAM) parserError("Unexpected end of stream") + } + + private fun readUntilEndOfLine(): BufferedSource { + val buffer = Buffer() + + while (peekChar() != CR) { + val byte = readByte() + buffer.writeByte(byte) + } + + return buffer + } + + private fun BufferedSource.readEhloLine(): String { + val text = readUtf8() + if (text.isEmpty()) { + parserError("EHLO line must not be empty") + } + + return text + } + + private fun BufferedSource.readTextString(): String { + val text = readUtf8() + if (text.isEmpty()) { + logger.log("'textstring' expected, but CR found instead") + } else if (text.any { it != HTAB && it.code !in 32..126 }) { + logger.log("Text contains characters not allowed in 'textstring'") + } + + return text + } + + private fun BufferedSource.maybeReadEnhancedStatusCode(replyCode: Int): StatusCode? { + val replyCode1 = replyCode / 100 + if (replyCode1 != 2 && replyCode1 != 4 && replyCode1 != 5) return null + + return try { + val peekBufferedSource = peek() + val statusCode = peekBufferedSource.readEnhancedStatusCode(replyCode1) + + val statusCodeLength = buffer.size - peekBufferedSource.buffer.size + skip(statusCodeLength) + + statusCode + } catch (e: SmtpResponseParserException) { + logger.log(e, "Error parsing enhanced status code") + null + } + } + + private fun BufferedSource.readEnhancedStatusCode(replyCode1: Int): StatusCode { + val statusClass = readStatusCodeClass(replyCode1) + expect(DOT) + val subject = readOneToThreeDigitNumber() + expect(DOT) + val detail = readOneToThreeDigitNumber() + + expect(SPACE) + + return StatusCode(statusClass, subject, detail) + } + + private fun BufferedSource.readStatusCodeClass(replyCode1: Int): StatusCodeClass { + val char = readChar() + val statusClass = when (char) { + '2' -> StatusCodeClass.SUCCESS + '4' -> StatusCodeClass.PERSISTENT_TRANSIENT_FAILURE + '5' -> StatusCodeClass.PERMANENT_FAILURE + else -> unexpectedCharacterError(char, logging = false) + } + + if (char != replyCode1.digitToChar()) { + parserError("Reply code doesn't match status code class: $replyCode1 != $char", logging = false) + } + + return statusClass + } + + private fun BufferedSource.readOneToThreeDigitNumber(): Int { + var number = readDigit() + repeat(2) { + if (peek().readChar() in '0'..'9') { + number *= 10 + number += readDigit() + } + } + + return number + } + + private fun BufferedSource.readDigit(): Int { + val char = readChar() + if (char !in '0'..'9') unexpectedCharacterError(char, logging = false) + + return char - '0' + } + + private fun BufferedSource.readChar(): Char { + if (exhausted()) parserError("Unexpected end of stream", logging = false) + + return readByte().toInt().toChar() + } + + private fun BufferedSource.expect(expectedChar: Char) { + val char = readChar() + if (char != expectedChar) unexpectedCharacterError(char, logging = false) + } + + private fun unexpectedCharacterError(char: Char, logging: Boolean = true): Nothing { + if (char.code in 33..126) { + parserError("Unexpected character: $char (${char.code})", logging) + } else { + parserError("Unexpected character: (${char.code})", logging) + } + } + + private fun parserError(message: String, logging: Boolean = true): Nothing { + if (logging && logger.isRawProtocolLoggingEnabled) { + logger.log("SMTP response data on parser error:\n%s", logBuffer.readUtf8().replace("\r\n", "\n")) + } + + throw SmtpResponseParserException(message) + } + + private fun Char.isCapitalAlphaDigit(): Boolean = this in '0'..'9' || this in 'A'..'Z' +} diff --git a/mail/protocols/smtp/src/main/java/com/fsck/k9/mail/transport/smtp/SmtpResponseParserException.kt b/mail/protocols/smtp/src/main/java/com/fsck/k9/mail/transport/smtp/SmtpResponseParserException.kt new file mode 100644 index 0000000000..e5ff7572d9 --- /dev/null +++ b/mail/protocols/smtp/src/main/java/com/fsck/k9/mail/transport/smtp/SmtpResponseParserException.kt @@ -0,0 +1,3 @@ +package com.fsck.k9.mail.transport.smtp + +class SmtpResponseParserException(message: String) : RuntimeException(message) diff --git a/mail/protocols/smtp/src/main/java/com/fsck/k9/mail/transport/smtp/StatusCodeClass.java b/mail/protocols/smtp/src/main/java/com/fsck/k9/mail/transport/smtp/StatusCodeClass.java index f56f289423..842863f190 100644 --- a/mail/protocols/smtp/src/main/java/com/fsck/k9/mail/transport/smtp/StatusCodeClass.java +++ b/mail/protocols/smtp/src/main/java/com/fsck/k9/mail/transport/smtp/StatusCodeClass.java @@ -7,7 +7,7 @@ enum StatusCodeClass { PERMANENT_FAILURE(5); - private final int codeClass; + final int codeClass; static StatusCodeClass parse(String statusCodeClassString) { diff --git a/mail/protocols/smtp/src/test/java/com/fsck/k9/mail/transport/smtp/SmtpResponseParserTest.kt b/mail/protocols/smtp/src/test/java/com/fsck/k9/mail/transport/smtp/SmtpResponseParserTest.kt new file mode 100644 index 0000000000..260b4036f5 --- /dev/null +++ b/mail/protocols/smtp/src/test/java/com/fsck/k9/mail/transport/smtp/SmtpResponseParserTest.kt @@ -0,0 +1,675 @@ +package com.fsck.k9.mail.transport.smtp + +import com.fsck.k9.mail.crlf +import com.fsck.k9.mail.filter.PeekableInputStream +import com.google.common.truth.Truth.assertThat +import org.junit.Assert.fail +import org.junit.Test + +class SmtpResponseParserTest { + private val logger = TestSmtpLogger() + + @Test + fun `read greeting`() { + val input = "220 smtp.domain.example ESMTP ready".toPeekableInputStream() + val parser = SmtpResponseParser(logger, input) + + val response = parser.readGreeting() + + assertThat(response.replyCode).isEqualTo(220) + assertThat(response.statusCode).isNull() + assertThat(response.texts).containsExactly("smtp.domain.example ESMTP ready") + assertInputExhausted(input) + } + + @Test + fun `read multi-line greeting`() { + val input = """ + 220-Greetings, stranger + 220 smtp.domain.example ESMTP ready + """.toPeekableInputStream() + val parser = SmtpResponseParser(logger, input) + + val response = parser.readGreeting() + + assertThat(response.replyCode).isEqualTo(220) + assertThat(response.statusCode).isNull() + assertThat(response.texts).containsExactly("Greetings, stranger", "smtp.domain.example ESMTP ready") + assertInputExhausted(input) + } + + @Test + fun `read EHLO response`() { + val input = """ + 250-smtp.domain.example greets 127.0.0.1 + 250-PIPELINING + 250-ENHANCEDSTATUSCODES + 250-8BITMIME + 250-SIZE 104857600 + 250-DELIVERBY + 250-AUTH PLAIN LOGIN CRAM-MD5 DIGEST-MD5 + 250 help + """.trimIndent() + val inputStream = input.toPeekableInputStream() + val parser = SmtpResponseParser(logger, inputStream) + + val response = parser.readHelloResponse() + + assertType(response) { hello -> + assertThat(hello.response.toLogString(omitText = false, linePrefix = "")).isEqualTo(input) + assertThat(hello.keywords.keys).containsExactly( + "PIPELINING", + "ENHANCEDSTATUSCODES", + "8BITMIME", + "SIZE", + "DELIVERBY", + "AUTH", + "HELP" + ) + assertThat(hello.keywords["PIPELINING"]).isEmpty() + assertThat(hello.keywords["SIZE"]).containsExactly("104857600") + assertThat(hello.keywords["AUTH"]).containsExactly("PLAIN", "LOGIN", "CRAM-MD5", "DIGEST-MD5") + } + assertInputExhausted(inputStream) + } + + @Test + fun `read EHLO response with only one line`() { + val input = "250 smtp.domain.example".toPeekableInputStream() + val parser = SmtpResponseParser(logger, input) + + val response = parser.readHelloResponse() + + assertType(response) { hello -> + assertThat(hello.response.replyCode).isEqualTo(250) + assertThat(hello.response.texts).containsExactly("smtp.domain.example") + assertThat(hello.keywords).isEmpty() + } + } + + @Test + fun `read EHLO error response`() { + val input = "421 Service not available".toPeekableInputStream() + val parser = SmtpResponseParser(logger, input) + + val response = parser.readHelloResponse() + + assertType(response) { error -> + assertThat(error.response.replyCode).isEqualTo(421) + assertThat(error.response.texts).containsExactly("Service not available") + } + } + + @Test + fun `read EHLO response with only reply code`() { + val input = "250".toPeekableInputStream() + val parser = SmtpResponseParser(logger, input) + + assertFailsWithMessage("Unexpected character: (13)") { + parser.readHelloResponse() + } + + assertThat(logger.logEntries).containsExactly( + LogEntry( + throwable = null, + message = """ + SMTP response data on parser error: + 250 + """.trimIndent() + ) + ) + } + + @Test + fun `read EHLO response with reply code not matching`() { + val input = """ + 250-smtp.domain.example + 220 + """.toPeekableInputStream() + val parser = SmtpResponseParser(logger, input) + + assertFailsWithMessage("Multi-line response with reply codes not matching: 250 != 220") { + parser.readHelloResponse() + } + + assertThat(logger.logEntries).containsExactly( + LogEntry( + throwable = null, + message = """ + SMTP response data on parser error: + 250-smtp.domain.example + 220 + """.trimIndent() + ) + ) + } + + @Test + fun `read EHLO response with invalid keyword`() { + val input = """ + 250-smtp.domain.example + 250 KEY:WORD + """.toPeekableInputStream() + val parser = SmtpResponseParser(logger, input) + + assertFailsWithMessage("EHLO keyword contains invalid character") { + parser.readHelloResponse() + } + + assertThat(logger.logEntries).containsExactly( + LogEntry( + throwable = null, + message = """ + SMTP response data on parser error: + 250-smtp.domain.example + 250 KEY:WORD + """.trimIndent() + ) + ) + } + + @Test + fun `read EHLO response with empty parameter`() { + val input = """ + 250-smtp.domain.example + 250 KEYWORD${" "} + """.toPeekableInputStream() + val parser = SmtpResponseParser(logger, input) + + assertFailsWithMessage("EHLO parameter must not be empty") { + parser.readHelloResponse() + } + + assertThat(logger.logEntries).containsExactly( + LogEntry( + throwable = null, + message = """ + SMTP response data on parser error: + 250-smtp.domain.example + 250 KEYWORD${" "} + """.trimIndent() + ) + ) + } + + @Test + fun `read EHLO response with invalid parameter`() { + val input = """ + 250-smtp.domain.example + 250 KEYWORD para${"\t"}meter + """.toPeekableInputStream() + val parser = SmtpResponseParser(logger, input) + + assertFailsWithMessage("EHLO parameter contains invalid character") { + parser.readHelloResponse() + } + + assertThat(logger.logEntries).containsExactly( + LogEntry( + throwable = null, + message = """ + SMTP response data on parser error: + 250-smtp.domain.example + 250 KEYWORD para${"\t"}meter + """.trimIndent() + ) + ) + } + + @Test + fun `positive response`() { + val input = "200 OK".toPeekableInputStream() + val parser = SmtpResponseParser(logger, input) + + val response = parser.readResponse(enhancedStatusCodes = false) + + assertThat(response.isNegativeResponse).isFalse() + } + + @Test + fun `negative response`() { + val input = "500 Oops".toPeekableInputStream() + val parser = SmtpResponseParser(logger, input) + + val response = parser.readResponse(enhancedStatusCodes = false) + + assertThat(response.isNegativeResponse).isTrue() + } + + @Test + fun `reply code only`() { + val input = "502".toPeekableInputStream() + val parser = SmtpResponseParser(logger, input) + + val response = parser.readResponse(enhancedStatusCodes = false) + + assertThat(response.replyCode).isEqualTo(502) + assertThat(response.statusCode).isNull() + assertThat(response.texts).isEmpty() + assertInputExhausted(input) + } + + @Test + fun `reply code and text`() { + val input = "250 OK".toPeekableInputStream() + val parser = SmtpResponseParser(logger, input) + + val response = parser.readResponse(enhancedStatusCodes = false) + + assertThat(response.replyCode).isEqualTo(250) + assertThat(response.statusCode).isNull() + assertThat(response.texts).containsExactly("OK") + assertInputExhausted(input) + } + + @Test + fun `reply code and text with enhanced status code`() { + val input = "250 2.1.0 Originator ok".toPeekableInputStream() + val parser = SmtpResponseParser(logger, input) + + val response = parser.readResponse(enhancedStatusCodes = true) + + assertThat(response.replyCode).isEqualTo(250) + assertThat(response.statusCode).isEqualTo( + StatusCode(statusClass = StatusCodeClass.SUCCESS, subject = 1, detail = 0) + ) + assertThat(response.texts).containsExactly("Originator ok") + assertInputExhausted(input) + } + + @Test + fun `enhancedStatusCodes enabled and 3xx reply code`() { + val input = "354 Ok Send data ending with .".toPeekableInputStream() + val parser = SmtpResponseParser(logger, input) + + val response = parser.readResponse(enhancedStatusCodes = true) + + assertThat(response.replyCode).isEqualTo(354) + assertThat(response.statusCode).isNull() + assertThat(response.texts).containsExactly("Ok Send data ending with .") + assertInputExhausted(input) + } + + @Test + fun `multi-line response with text`() { + val input = """ + 500-Line one + 500 Line two + """.toPeekableInputStream() + val parser = SmtpResponseParser(logger, input) + + val response = parser.readResponse(enhancedStatusCodes = false) + + assertThat(response.replyCode).isEqualTo(500) + assertThat(response.statusCode).isNull() + assertThat(response.texts).containsExactly("Line one", "Line two") + assertInputExhausted(input) + } + + @Test + fun `multi-line response with empty textstring`() { + val input = """ + 500- + 500 Line two + """.toPeekableInputStream() + val parser = SmtpResponseParser(logger, input) + + val response = parser.readResponse(enhancedStatusCodes = false) + + assertThat(response.replyCode).isEqualTo(500) + assertThat(response.statusCode).isNull() + assertThat(response.texts).containsExactly("", "Line two") + assertInputExhausted(input) + } + + @Test + fun `multi-line response without text on last line`() { + val input = """ + 500-Line one + 500-Line two + 500 + """.toPeekableInputStream() + val parser = SmtpResponseParser(logger, input) + + val response = parser.readResponse(enhancedStatusCodes = false) + + assertThat(response.replyCode).isEqualTo(500) + assertThat(response.statusCode).isNull() + assertThat(response.texts).containsExactly("Line one", "Line two") + assertInputExhausted(input) + } + + @Test + fun `multi-line response with enhanced status code`() { + val input = """ + 250-2.1.0 Sender + 250 2.1.0 OK + """.toPeekableInputStream() + val parser = SmtpResponseParser(logger, input) + + val response = parser.readResponse(enhancedStatusCodes = true) + + assertThat(response.replyCode).isEqualTo(250) + assertThat(response.statusCode).isEqualTo( + StatusCode(statusClass = StatusCodeClass.SUCCESS, subject = 1, detail = 0) + ) + assertThat(response.texts).containsExactly("Sender ", "OK") + assertInputExhausted(input) + } + + @Test + fun `read multiple responses`() { + val input = """ + 250 Sender OK + 250 Recipient OK + """.toPeekableInputStream() + val parser = SmtpResponseParser(logger, input) + + val responseOne = parser.readResponse(enhancedStatusCodes = false) + + assertThat(responseOne.replyCode).isEqualTo(250) + assertThat(responseOne.statusCode).isNull() + assertThat(responseOne.texts).containsExactly("Sender OK") + + val responseTwo = parser.readResponse(enhancedStatusCodes = false) + + assertThat(responseTwo.replyCode).isEqualTo(250) + assertThat(responseTwo.statusCode).isNull() + assertThat(responseTwo.texts).containsExactly("Recipient OK") + assertInputExhausted(input) + } + + @Test + fun `multi-line response with reply codes not matching`() { + val input = """ + 200-Line one + 500 Line two + """.toPeekableInputStream() + val parser = SmtpResponseParser(logger, input) + + assertFailsWithMessage("Multi-line response with reply codes not matching: 200 != 500") { + parser.readResponse(enhancedStatusCodes = false) + } + + assertThat(logger.logEntries).containsExactly( + LogEntry( + throwable = null, + message = """ + SMTP response data on parser error: + 200-Line one + 500 + """.trimIndent() + ) + ) + } + + @Test + fun `multi-line response with reply codes not matching and raw protocol logging disabled`() { + val input = """ + 200-Line one + 500 Line two + """.toPeekableInputStream() + val logger = TestSmtpLogger(isRawProtocolLoggingEnabled = false) + val parser = SmtpResponseParser(logger, input) + + assertFailsWithMessage("Multi-line response with reply codes not matching: 200 != 500") { + parser.readResponse(enhancedStatusCodes = false) + } + + assertThat(logger.logEntries).isEmpty() + } + + @Test + fun `invalid 1st reply code digit`() { + val input = "611".toPeekableInputStream() + val parser = SmtpResponseParser(logger, input) + + assertFailsWithMessage("Unsupported 1st reply code digit: 6") { + parser.readResponse(enhancedStatusCodes = false) + } + } + + @Test + fun `invalid 2nd reply code digit should only produce a log entry`() { + val input = "280 Something".toPeekableInputStream() + val parser = SmtpResponseParser(logger, input) + + val response = parser.readResponse(enhancedStatusCodes = false) + + assertThat(response.replyCode).isEqualTo(280) + assertThat(response.statusCode).isNull() + assertThat(response.texts).containsExactly("Something") + assertThat(logger.logEntries).containsExactly( + LogEntry(throwable = null, message = "2nd digit of reply code outside of specified range (0..5): 8") + ) + } + + @Test + fun `invalid 3rd reply code digit`() { + val input = "20x".toPeekableInputStream() + val parser = SmtpResponseParser(logger, input) + + assertFailsWithMessage("Unexpected character: x (120)") { + parser.readResponse(enhancedStatusCodes = false) + } + + assertThat(logger.logEntries).containsExactly( + LogEntry( + throwable = null, + message = """ + SMTP response data on parser error: + 20x + """.trimIndent() + ) + ) + } + + @Test + fun `end of stream after reply code`() { + val input = PeekableInputStream("200".byteInputStream()) + val parser = SmtpResponseParser(logger, input) + + assertFailsWithMessage("Unexpected end of stream") { + parser.readResponse(enhancedStatusCodes = false) + } + + assertThat(logger.logEntries).containsExactly( + LogEntry( + throwable = null, + message = """ + SMTP response data on parser error: + 200 + """.trimIndent() + ) + ) + } + + @Test + fun `response ending with CR only`() { + val input = PeekableInputStream("200\r".byteInputStream()) + val parser = SmtpResponseParser(logger, input) + + assertFailsWithMessage("Unexpected end of stream") { + parser.readResponse(enhancedStatusCodes = false) + } + } + + @Test + fun `response ending with LF only`() { + val input = PeekableInputStream("200\n".byteInputStream()) + val parser = SmtpResponseParser(logger, input) + + assertFailsWithMessage("Unexpected character: (10)") { + parser.readResponse(enhancedStatusCodes = false) + } + + assertThat(logger.logEntries).containsExactly( + LogEntry( + throwable = null, + message = """ + SMTP response data on parser error: + 200 + """.trimIndent() + ) + ) + } + + @Test + fun `reply code with space but without text`() { + val input = "200 ".toPeekableInputStream() + val parser = SmtpResponseParser(logger, input) + + val response = parser.readResponse(enhancedStatusCodes = false) + + assertThat(response.replyCode).isEqualTo(200) + assertThat(response.statusCode).isNull() + assertThat(response.texts).isEmpty() + assertInputExhausted(input) + assertThat(logger.logEntries).containsExactly( + LogEntry(throwable = null, message = "'textstring' expected, but CR found instead") + ) + } + + @Test + fun `text containing non-ASCII character`() { + val input = "200 über".toPeekableInputStream() + val parser = SmtpResponseParser(logger, input) + + val response = parser.readResponse(enhancedStatusCodes = false) + + assertThat(response.replyCode).isEqualTo(200) + assertThat(response.statusCode).isNull() + assertThat(response.texts).containsExactly("über") + assertInputExhausted(input) + assertThat(logger.logEntries).containsExactly( + LogEntry(throwable = null, message = "Text contains characters not allowed in 'textstring'") + ) + } + + @Test + fun `enhanced status code class does not match reply code`() { + val input = "250 5.0.0 text".toPeekableInputStream() + val parser = SmtpResponseParser(logger, input) + + val response = parser.readResponse(enhancedStatusCodes = true) + + assertThat(response.replyCode).isEqualTo(250) + assertThat(response.statusCode).isNull() + assertThat(response.texts).containsExactly("5.0.0 text") + assertInputExhausted(input) + assertThat(logger.logEntries).hasSize(1) + logger.logEntries.first().let { logEntry -> + assertThat(logEntry.message).isEqualTo("Error parsing enhanced status code") + assertThat(logEntry.throwable?.message).isEqualTo("Reply code doesn't match status code class: 2 != 5") + } + } + + @Test + fun `response with invalid enhanced status code subject`() { + val input = "250 2.1000.0 Text".toPeekableInputStream() + val parser = SmtpResponseParser(logger, input) + + val response = parser.readResponse(enhancedStatusCodes = true) + + assertThat(response.replyCode).isEqualTo(250) + assertThat(response.statusCode).isNull() + assertThat(response.texts).containsExactly("2.1000.0 Text") + assertInputExhausted(input) + assertThat(logger.logEntries).hasSize(1) + logger.logEntries.first().let { logEntry -> + assertThat(logEntry.message).isEqualTo("Error parsing enhanced status code") + assertThat(logEntry.throwable?.message).isEqualTo("Unexpected character: 0 (48)") + } + } + + @Test + fun `response with invalid enhanced status code detail`() { + val input = "250 2.0.1000 Text".toPeekableInputStream() + val parser = SmtpResponseParser(logger, input) + + val response = parser.readResponse(enhancedStatusCodes = true) + + assertThat(response.replyCode).isEqualTo(250) + assertThat(response.statusCode).isNull() + assertThat(response.texts).containsExactly("2.0.1000 Text") + assertInputExhausted(input) + assertThat(logger.logEntries).hasSize(1) + logger.logEntries.first().let { logEntry -> + assertThat(logEntry.message).isEqualTo("Error parsing enhanced status code") + assertThat(logEntry.throwable?.message).isEqualTo("Unexpected character: 0 (48)") + } + } + + @Test + fun `response with missing enhanced status code`() { + // Yahoo has been observed to send replies without enhanced status code even though the EHLO keyword is present + val input = "550 Request failed; Mailbox unavailable".toPeekableInputStream() + val parser = SmtpResponseParser(logger, input) + + val response = parser.readResponse(enhancedStatusCodes = true) + + assertThat(response.replyCode).isEqualTo(550) + assertThat(response.statusCode).isNull() + assertThat(response.texts).containsExactly("Request failed; Mailbox unavailable") + assertInputExhausted(input) + assertThat(logger.logEntries).hasSize(1) + logger.logEntries.first().let { logEntry -> + assertThat(logEntry.message).isEqualTo("Error parsing enhanced status code") + assertThat(logEntry.throwable?.message).isEqualTo("Unexpected character: R (82)") + } + } + + @Test + fun `multi-line response with enhanced status code missing in last line`() { + val input = """ + 550-5.2.1 Request failed + 550 Mailbox unavailable + """.toPeekableInputStream() + val parser = SmtpResponseParser(logger, input) + + assertFailsWithMessage( + "Multi-line response with enhanced status codes not matching: " + + "StatusCode(statusClass=PERMANENT_FAILURE, subject=2, detail=1) != null" + ) { + parser.readResponse(enhancedStatusCodes = true) + } + } + + @Test + fun `multi-line response with missing enhanced status code`() { + val input = """ + 550-Request failed + 550 Mailbox unavailable + """.toPeekableInputStream() + val parser = SmtpResponseParser(logger, input) + + val response = parser.readResponse(enhancedStatusCodes = true) + + assertThat(response.replyCode).isEqualTo(550) + assertThat(response.statusCode).isNull() + assertThat(response.texts).containsExactly("Request failed", "Mailbox unavailable") + assertInputExhausted(input) + } + + private fun assertInputExhausted(input: PeekableInputStream) { + assertThat(input.read()).isEqualTo(-1) + } + + private fun assertFailsWithMessage(expectedMessage: String, block: () -> Unit) { + try { + block() + fail("Expected SmtpResponseParserException") + } catch (e: SmtpResponseParserException) { + assertThat(e).hasMessageThat().isEqualTo(expectedMessage) + } + } + + private fun String.toPeekableInputStream(): PeekableInputStream { + return PeekableInputStream((this.trimIndent().crlf() + "\r\n").byteInputStream()) + } + + private inline fun assertType(actual: Any, block: (T) -> Unit) { + assertThat(actual).isInstanceOf(T::class.java) + block(actual as T) + } +} diff --git a/mail/protocols/smtp/src/test/java/com/fsck/k9/mail/transport/smtp/SmtpResponseTest.kt b/mail/protocols/smtp/src/test/java/com/fsck/k9/mail/transport/smtp/SmtpResponseTest.kt new file mode 100644 index 0000000000..69b29414b4 --- /dev/null +++ b/mail/protocols/smtp/src/test/java/com/fsck/k9/mail/transport/smtp/SmtpResponseTest.kt @@ -0,0 +1,172 @@ +package com.fsck.k9.mail.transport.smtp + +import com.google.common.truth.Truth.assertThat +import org.junit.Test + +class SmtpResponseTest { + @Test + fun `log reply code only`() { + val response = SmtpResponse( + replyCode = 200, + statusCode = null, + texts = emptyList() + ) + + val output = response.toLogString(omitText = false, linePrefix = "SMTP <<< ") + + assertThat(output).isEqualTo("SMTP <<< 200") + } + + @Test + fun `log reply code only with omitText = true`() { + val response = SmtpResponse( + replyCode = 200, + statusCode = null, + texts = emptyList() + ) + + val output = response.toLogString(omitText = true, linePrefix = "SMTP <<< ") + + assertThat(output).isEqualTo("SMTP <<< 200") + } + + @Test + fun `log reply code and text`() { + val response = SmtpResponse( + replyCode = 200, + statusCode = null, + texts = listOf("OK") + ) + + val output = response.toLogString(omitText = false, linePrefix = "SMTP <<< ") + + assertThat(output).isEqualTo("SMTP <<< 200 OK") + } + + @Test + fun `log reply code and text with omitText = true`() { + val response = SmtpResponse( + replyCode = 250, + statusCode = null, + texts = listOf("Sender OK") + ) + + val output = response.toLogString(omitText = true, linePrefix = "SMTP <<< ") + + assertThat(output).isEqualTo("SMTP <<< 250 [omitted]") + } + + @Test + fun `log reply code and status code`() { + val response = SmtpResponse( + replyCode = 200, + statusCode = StatusCode(statusClass = StatusCodeClass.SUCCESS, subject = 0, detail = 0), + texts = emptyList() + ) + + val output = response.toLogString(omitText = false, linePrefix = "SMTP <<< ") + + assertThat(output).isEqualTo("SMTP <<< 200 2.0.0") + } + + @Test + fun `log reply code and status code with omitText = true`() { + val response = SmtpResponse( + replyCode = 200, + statusCode = StatusCode(statusClass = StatusCodeClass.SUCCESS, subject = 0, detail = 0), + texts = emptyList() + ) + + val output = response.toLogString(omitText = true, linePrefix = "SMTP <<< ") + + assertThat(output).isEqualTo("SMTP <<< 200 2.0.0") + } + + @Test + fun `log reply code, status code, and text`() { + val response = SmtpResponse( + replyCode = 200, + statusCode = StatusCode(statusClass = StatusCodeClass.SUCCESS, subject = 0, detail = 0), + texts = listOf("OK") + ) + + val output = response.toLogString(omitText = false, linePrefix = "SMTP <<< ") + + assertThat(output).isEqualTo("SMTP <<< 200 2.0.0 OK") + } + + @Test + fun `log reply code, status code, and text with omitText = true`() { + val response = SmtpResponse( + replyCode = 200, + statusCode = StatusCode(statusClass = StatusCodeClass.SUCCESS, subject = 0, detail = 0), + texts = listOf("OK") + ) + + val output = response.toLogString(omitText = true, linePrefix = "SMTP <<< ") + + assertThat(output).isEqualTo("SMTP <<< 200 2.0.0 [omitted]") + } + + @Test + fun `log reply code and multi-line text`() { + val response = SmtpResponse( + replyCode = 250, + statusCode = null, + texts = listOf("Sender ", "OK") + ) + + val output = response.toLogString(omitText = false, linePrefix = "SMTP <<< ") + + assertThat(output).isEqualTo( + """ + SMTP <<< 250-Sender + SMTP <<< 250 OK + """.trimIndent() + ) + } + + @Test + fun `log reply code and multi-line text with omitText = true`() { + val response = SmtpResponse( + replyCode = 250, + statusCode = null, + texts = listOf("Sender ", "OK") + ) + + val output = response.toLogString(omitText = true, linePrefix = "SMTP <<< ") + + assertThat(output).isEqualTo("SMTP <<< 250 [omitted]") + } + + @Test + fun `log reply code, status code, and multi-line text`() { + val response = SmtpResponse( + replyCode = 250, + statusCode = StatusCode(statusClass = StatusCodeClass.SUCCESS, subject = 1, detail = 0), + texts = listOf("Sender ", "OK") + ) + + val output = response.toLogString(omitText = false, linePrefix = "SMTP <<< ") + + assertThat(output).isEqualTo( + """ + SMTP <<< 250-2.1.0 Sender + SMTP <<< 250 2.1.0 OK + """.trimIndent() + ) + } + + @Test + fun `log reply code, status code, and multi-line text with omitText = true`() { + val response = SmtpResponse( + replyCode = 250, + statusCode = StatusCode(statusClass = StatusCodeClass.SUCCESS, subject = 1, detail = 0), + texts = listOf("Sender ", "OK") + ) + + val output = response.toLogString(omitText = true, linePrefix = "SMTP <<< ") + + assertThat(output).isEqualTo("SMTP <<< 250 2.1.0 [omitted]") + } +} diff --git a/mail/protocols/smtp/src/test/java/com/fsck/k9/mail/transport/smtp/TestSmtpLogger.kt b/mail/protocols/smtp/src/test/java/com/fsck/k9/mail/transport/smtp/TestSmtpLogger.kt new file mode 100644 index 0000000000..ca3e263166 --- /dev/null +++ b/mail/protocols/smtp/src/test/java/com/fsck/k9/mail/transport/smtp/TestSmtpLogger.kt @@ -0,0 +1,15 @@ +package com.fsck.k9.mail.transport.smtp + +class TestSmtpLogger(override val isRawProtocolLoggingEnabled: Boolean = true) : SmtpLogger { + val logEntries = mutableListOf() + + override fun log(throwable: Throwable?, message: String, vararg args: Any?) { + val formattedMessage = String.format(message, *args) + logEntries.add(LogEntry(throwable, formattedMessage)) + } +} + +data class LogEntry( + val throwable: Throwable?, + val message: String +) -- GitLab From d13352a4cd7f5ea540eba738155ff377ee48da62 Mon Sep 17 00:00:00 2001 From: cketti Date: Wed, 23 Mar 2022 04:07:37 +0100 Subject: [PATCH 05/75] Use new `SmtpResponseParser` in `SmtpTransport` --- .../EnhancedNegativeSmtpReplyException.java | 12 +- .../k9/mail/transport/smtp/SmtpResponse.kt | 3 + .../k9/mail/transport/smtp/SmtpTransport.java | 268 ++++++------------ .../mail/transport/smtp/StatusCodeClass.java | 12 - .../mail/transport/smtp/StatusCodeDetail.java | 80 ------ .../transport/smtp/StatusCodeSubject.java | 31 -- .../transport/smtp/SmtpTransportTest.java | 8 +- 7 files changed, 100 insertions(+), 314 deletions(-) delete mode 100644 mail/protocols/smtp/src/main/java/com/fsck/k9/mail/transport/smtp/StatusCodeDetail.java delete mode 100644 mail/protocols/smtp/src/main/java/com/fsck/k9/mail/transport/smtp/StatusCodeSubject.java diff --git a/mail/protocols/smtp/src/main/java/com/fsck/k9/mail/transport/smtp/EnhancedNegativeSmtpReplyException.java b/mail/protocols/smtp/src/main/java/com/fsck/k9/mail/transport/smtp/EnhancedNegativeSmtpReplyException.java index 7be1e11f73..bb43d59171 100644 --- a/mail/protocols/smtp/src/main/java/com/fsck/k9/mail/transport/smtp/EnhancedNegativeSmtpReplyException.java +++ b/mail/protocols/smtp/src/main/java/com/fsck/k9/mail/transport/smtp/EnhancedNegativeSmtpReplyException.java @@ -2,17 +2,11 @@ package com.fsck.k9.mail.transport.smtp; class EnhancedNegativeSmtpReplyException extends NegativeSmtpReplyException { - private final StatusCodeClass statusCodeClass; - private final StatusCodeSubject statusCodeSubject; - private final StatusCodeDetail statusCodeDetail; + public final StatusCode statusCode; - EnhancedNegativeSmtpReplyException(int replyCode, StatusCodeClass statusCodeClass, - StatusCodeSubject statusCodeSubject, StatusCodeDetail statusCodeDetail, - String replyText) { + EnhancedNegativeSmtpReplyException(int replyCode, String replyText, StatusCode statusCode) { super(replyCode, replyText); - this.statusCodeClass = statusCodeClass; - this.statusCodeSubject = statusCodeSubject; - this.statusCodeDetail = statusCodeDetail; + this.statusCode = statusCode; } } diff --git a/mail/protocols/smtp/src/main/java/com/fsck/k9/mail/transport/smtp/SmtpResponse.kt b/mail/protocols/smtp/src/main/java/com/fsck/k9/mail/transport/smtp/SmtpResponse.kt index 75bf0fadfb..0a13b0ea42 100644 --- a/mail/protocols/smtp/src/main/java/com/fsck/k9/mail/transport/smtp/SmtpResponse.kt +++ b/mail/protocols/smtp/src/main/java/com/fsck/k9/mail/transport/smtp/SmtpResponse.kt @@ -7,6 +7,9 @@ internal data class SmtpResponse( ) { val isNegativeResponse = replyCode >= 400 + val joinedText: String + get() = texts.joinToString(separator = " ") + fun toLogString(omitText: Boolean, linePrefix: String): String { return buildString { if (omitText) { diff --git a/mail/protocols/smtp/src/main/java/com/fsck/k9/mail/transport/smtp/SmtpTransport.java b/mail/protocols/smtp/src/main/java/com/fsck/k9/mail/transport/smtp/SmtpTransport.java index 0019198347..ff1c232b16 100644 --- a/mail/protocols/smtp/src/main/java/com/fsck/k9/mail/transport/smtp/SmtpTransport.java +++ b/mail/protocols/smtp/src/main/java/com/fsck/k9/mail/transport/smtp/SmtpTransport.java @@ -13,8 +13,6 @@ import java.net.Socket; import java.net.SocketAddress; import java.net.SocketException; import java.security.GeneralSecurityException; -import java.util.ArrayList; -import java.util.Arrays; import java.util.HashMap; import java.util.LinkedHashSet; import java.util.LinkedList; @@ -26,6 +24,8 @@ import java.util.Set; import android.text.TextUtils; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import com.fsck.k9.mail.Address; import com.fsck.k9.mail.AuthType; import com.fsck.k9.mail.Authentication; @@ -73,12 +73,30 @@ public class SmtpTransport extends Transport { private Socket socket; private PeekableInputStream inputStream; private OutputStream outputStream; + private SmtpResponseParser responseParser; + private boolean is8bitEncodingAllowed; private boolean isEnhancedStatusCodesProvided; private int largestAcceptableMessage; private boolean retryXoauthWithNewToken; private boolean isPipeliningSupported; + private final SmtpLogger logger = new SmtpLogger() { + @Override + public void log(@NonNull String message, @Nullable Object... args) { + log(null, message, args); + } + + @Override + public boolean isRawProtocolLoggingEnabled() { + return K9MailLib.isDebug(); + } + + @Override + public void log(@Nullable Throwable throwable, @NonNull String message, @Nullable Object... args) { + Timber.v(throwable, message, args); + } + }; public SmtpTransport(ServerSettings serverSettings, TrustedSocketFactory trustedSocketFactory, OAuth2TokenProvider oauthTokenProvider) { @@ -130,14 +148,14 @@ public class SmtpTransport extends Transport { socket.setSoTimeout(SOCKET_READ_TIMEOUT); inputStream = new PeekableInputStream(new BufferedInputStream(socket.getInputStream(), 1024)); + responseParser = new SmtpResponseParser(logger, inputStream); outputStream = new BufferedOutputStream(socket.getOutputStream(), 1024); - // Eat the banner - executeCommand(null); + readGreeting(); String hostnameToReportInHelo = buildHostnameToReport(); - Map extensions = sendHello(hostnameToReportInHelo); + Map> extensions = sendHello(hostnameToReportInHelo); is8bitEncodingAllowed = extensions.containsKey("8BITMIME"); isEnhancedStatusCodesProvided = extensions.containsKey("ENHANCEDSTATUSCODES"); @@ -155,6 +173,7 @@ public class SmtpTransport extends Transport { inputStream = new PeekableInputStream(new BufferedInputStream(socket.getInputStream(), 1024)); + responseParser = new SmtpResponseParser(logger, inputStream); outputStream = new BufferedOutputStream(socket.getOutputStream(), 1024); /* * Now resend the EHLO. Required by RFC2487 Sec. 5.2, and more specifically, @@ -180,15 +199,15 @@ public class SmtpTransport extends Transport { boolean authCramMD5Supported = false; boolean authExternalSupported = false; boolean authXoauth2Supported = false; - if (extensions.containsKey("AUTH")) { - List saslMech = Arrays.asList(extensions.get("AUTH").split(" ")); + List saslMech = extensions.get("AUTH"); + if (saslMech != null) { authLoginSupported = saslMech.contains("LOGIN"); authPlainSupported = saslMech.contains("PLAIN"); authCramMD5Supported = saslMech.contains("CRAM-MD5"); authExternalSupported = saslMech.contains("EXTERNAL"); authXoauth2Supported = saslMech.contains("XOAUTH2"); } - parseOptionalSizeValue(extensions); + parseOptionalSizeValue(extensions.get("SIZE")); if (!TextUtils.isEmpty(username) && (!TextUtils.isEmpty(password) || @@ -301,6 +320,17 @@ public class SmtpTransport extends Transport { } } + private void readGreeting() { + SmtpResponse smtpResponse = responseParser.readGreeting(); + logResponse(smtpResponse, false); + } + + private void logResponse(SmtpResponse smtpResponse, boolean omitText) { + if (K9MailLib.isDebug()) { + Timber.v("%s", smtpResponse.toLogString(omitText, "SMTP <<< ")); + } + } + private String buildHostnameToReport() { InetAddress localAddress = socket.getLocalAddress(); @@ -312,16 +342,14 @@ public class SmtpTransport extends Transport { } } - private void parseOptionalSizeValue(Map extensions) { - if (extensions.containsKey("SIZE")) { - String optionalsizeValue = extensions.get("SIZE"); - if (optionalsizeValue != null && !"".equals(optionalsizeValue)) { - try { - largestAcceptableMessage = Integer.parseInt(optionalsizeValue); - } catch (NumberFormatException e) { - if (K9MailLib.isDebug() && DEBUG_PROTOCOL_SMTP) { - Timber.d(e, "Tried to parse %s and get an int", optionalsizeValue); - } + private void parseOptionalSizeValue(List sizeParameters) { + if (sizeParameters != null && sizeParameters.size() >= 1) { + String sizeParameter = sizeParameters.get(0); + try { + largestAcceptableMessage = Integer.parseInt(sizeParameter); + } catch (NumberFormatException e) { + if (K9MailLib.isDebug() && DEBUG_PROTOCOL_SMTP) { + Timber.d(e, "Tried to parse %s and get an int", sizeParameter); } } } @@ -339,25 +367,20 @@ public class SmtpTransport extends Transport { * @param host * The EHLO/HELO parameter as defined by the RFC. * - * @return A (possibly empty) {@code Map} of extensions (upper case) and - * their parameters (possibly 0 length) as returned by the EHLO command - * - * @throws IOException - * In case of a network error. - * @throws MessagingException - * In case of a malformed response. + * @return A (possibly empty) {@code Map>} of extensions (upper case) and + * their parameters (possibly empty) as returned by the EHLO command. */ - private Map sendHello(String host) throws IOException, MessagingException { - Map extensions = new HashMap<>(); - try { - List results = executeCommand("EHLO %s", host).results; - // Remove the EHLO greeting response - results.remove(0); - for (String result : results) { - String[] pair = result.split(" ", 2); - extensions.put(pair[0].toUpperCase(Locale.US), pair.length == 1 ? "" : pair[1]); - } - } catch (NegativeSmtpReplyException e) { + private Map> sendHello(String host) throws IOException, MessagingException { + writeLine("EHLO " + host, false); + + SmtpHelloResponse helloResponse = responseParser.readHelloResponse(); + logResponse(helloResponse.getResponse(), false); + + if (helloResponse instanceof SmtpHelloResponse.Hello) { + SmtpHelloResponse.Hello hello = (SmtpHelloResponse.Hello) helloResponse; + + return hello.getKeywords(); + } else { if (K9MailLib.isDebug()) { Timber.v("Server doesn't support the EHLO command. Trying HELO..."); } @@ -367,8 +390,9 @@ public class SmtpTransport extends Transport { } catch (NegativeSmtpReplyException e2) { Timber.w("Server doesn't support the HELO command. Continuing anyway."); } + + return new HashMap<>(); } - return extensions; } @Override @@ -464,28 +488,11 @@ public class SmtpTransport extends Transport { IOUtils.closeQuietly(outputStream); IOUtils.closeQuietly(socket); inputStream = null; + responseParser = null; outputStream = null; socket = null; } - private String readLine() throws IOException { - StringBuilder sb = new StringBuilder(); - int d; - while ((d = inputStream.read()) != -1) { - char c = (char) d; - if (c == '\n') { - break; - } else if (c != '\r') { - sb.append(c); - } - } - String ret = sb.toString(); - if (K9MailLib.isDebug() && DEBUG_PROTOCOL_SMTP) - Timber.d("SMTP <<< %s", ret); - - return ret; - } - private void writeLine(String s, boolean sensitive) throws IOException { if (K9MailLib.isDebug() && DEBUG_PROTOCOL_SMTP) { final String commandToLog; @@ -509,103 +516,40 @@ public class SmtpTransport extends Transport { outputStream.flush(); } - private static class CommandResponse { - - private final int replyCode; - private final List results; - - CommandResponse(int replyCode, List results) { - this.replyCode = replyCode; - this.results = results; - } - } - - private CommandResponse executeSensitiveCommand(String format, Object... args) + private SmtpResponse executeSensitiveCommand(String format, Object... args) throws IOException, MessagingException { return executeCommand(true, format, args); } - private CommandResponse executeCommand(String format, Object... args) throws IOException, MessagingException { + private SmtpResponse executeCommand(String format, Object... args) throws IOException, MessagingException { return executeCommand(false, format, args); } - private CommandResponse executeCommand(boolean sensitive, String format, Object... args) + private SmtpResponse executeCommand(boolean sensitive, String format, Object... args) throws IOException, MessagingException { - List results = new ArrayList<>(); - if (format != null) { - String command = String.format(Locale.ROOT, format, args); - writeLine(command, sensitive); - } + String command = String.format(Locale.ROOT, format, args); + writeLine(command, sensitive); - String line = readCommandResponseLine(results); + SmtpResponse response = responseParser.readResponse(isEnhancedStatusCodesProvided); + logResponse(response, sensitive); - int length = line.length(); - if (length < 1) { - throw new MessagingException("SMTP response is 0 length"); + if (response.isNegativeResponse()) { + throw buildNegativeSmtpReplyException(response); } - int replyCode = -1; - if (length >= 3) { - try { - replyCode = Integer.parseInt(line.substring(0, 3)); - } catch (NumberFormatException e) { /* ignore */ } - } - - char replyCodeCategory = line.charAt(0); - boolean isReplyCodeErrorCategory = (replyCodeCategory == '4') || (replyCodeCategory == '5'); - if (isReplyCodeErrorCategory) { - if (isEnhancedStatusCodesProvided) { - throw buildEnhancedNegativeSmtpReplyException(replyCode, results); - } else { - String replyText = TextUtils.join(" ", results); - throw new NegativeSmtpReplyException(replyCode, replyText); - } - } - - return new CommandResponse(replyCode, results); + return response; } - private MessagingException buildEnhancedNegativeSmtpReplyException(int replyCode, List results) { - StatusCodeClass statusCodeClass = null; - StatusCodeSubject statusCodeSubject = null; - StatusCodeDetail statusCodeDetail = null; - - String message = ""; - for (String resultLine : results) { - message += resultLine.split(" ", 2)[1] + " "; - } - if (results.size() > 0) { - String[] statusCodeParts = results.get(0).split(" ", 2)[0].split("\\."); - - statusCodeClass = StatusCodeClass.parse(statusCodeParts[0]); - statusCodeSubject = StatusCodeSubject.parse(statusCodeParts[1]); - statusCodeDetail = StatusCodeDetail.parse(statusCodeSubject, statusCodeParts[2]); - } - - return new EnhancedNegativeSmtpReplyException(replyCode, statusCodeClass, statusCodeSubject, statusCodeDetail, - message.trim()); - } - - - /* - * Read lines as long as the length is 4 or larger, e.g. "220-banner text here". - * Shorter lines are either errors of contain only a reply code. - */ - private String readCommandResponseLine(List results) throws IOException { - String line = readLine(); - while (line.length() >= 4) { - if (line.length() > 4) { - // Everything after the first four characters goes into the results array. - results.add(line.substring(4)); - } + private NegativeSmtpReplyException buildNegativeSmtpReplyException(SmtpResponse response) { + int replyCode = response.getReplyCode(); + StatusCode statusCode = response.getStatusCode(); + String replyText = response.getJoinedText(); - if (line.charAt(3) != '-') { - // If the fourth character isn't "-" this is the last line of the response. - break; - } - line = readLine(); + if (statusCode != null) { + return new EnhancedNegativeSmtpReplyException(replyCode, replyText, statusCode); + } else { + return new NegativeSmtpReplyException(replyCode, replyText); } - return line; } private void executePipelinedCommands(Queue pipelinedCommands) throws IOException { @@ -615,19 +559,15 @@ public class SmtpTransport extends Transport { } private void readPipelinedResponse(Queue pipelinedCommands) throws IOException, MessagingException { - String responseLine; - List results = new ArrayList<>(); + boolean omitText = false; MessagingException firstException = null; + for (int i = 0, size = pipelinedCommands.size(); i < size; i++) { - results.clear(); - responseLine = readCommandResponseLine(results); - try { - responseLineToCommandResponse(responseLine, results); + SmtpResponse response = responseParser.readResponse(isEnhancedStatusCodesProvided); + logResponse(response, omitText); - } catch (MessagingException exception) { - if (firstException == null) { - firstException = exception; - } + if (response.isNegativeResponse() && firstException == null) { + firstException = buildNegativeSmtpReplyException(response); } } @@ -636,34 +576,6 @@ public class SmtpTransport extends Transport { } } - private CommandResponse responseLineToCommandResponse(String line, List results) throws MessagingException { - int length = line.length(); - if (length < 1) { - throw new MessagingException("SMTP response to line is 0 length"); - } - - int replyCode = -1; - if (length >= 3) { - try { - replyCode = Integer.parseInt(line.substring(0, 3)); - } catch (NumberFormatException e) { /* ignore */ } - } - - char replyCodeCategory = line.charAt(0); - boolean isReplyCodeErrorCategory = (replyCodeCategory == '4') || (replyCodeCategory == '5'); - if (isReplyCodeErrorCategory) { - if (isEnhancedStatusCodesProvided) { - throw buildEnhancedNegativeSmtpReplyException(replyCode, results); - } else { - String replyText = TextUtils.join(" ", results); - throw new NegativeSmtpReplyException(replyCode, replyText); - } - } - - return new CommandResponse(replyCode, results); - } - - private void saslAuthLogin() throws MessagingException, IOException { try { executeCommand("AUTH LOGIN"); @@ -694,7 +606,7 @@ public class SmtpTransport extends Transport { private void saslAuthCramMD5() throws MessagingException, IOException { - List respList = executeCommand("AUTH CRAM-MD5").results; + List respList = executeCommand("AUTH CRAM-MD5").getTexts(); if (respList.size() != 1) { throw new MessagingException("Unable to negotiate CRAM-MD5"); } @@ -765,10 +677,10 @@ public class SmtpTransport extends Transport { private void attemptXoauth2(String username) throws MessagingException, IOException { String token = oauthTokenProvider.getToken(username, OAuth2TokenProvider.OAUTH2_TIMEOUT); String authString = Authentication.computeXoauth(username, token); - CommandResponse response = executeSensitiveCommand("AUTH XOAUTH2 %s", authString); + SmtpResponse response = executeSensitiveCommand("AUTH XOAUTH2 %s", authString); - if (response.replyCode == SMTP_CONTINUE_REQUEST) { - String replyText = TextUtils.join("", response.results); + if (response.getReplyCode() == SMTP_CONTINUE_REQUEST) { + String replyText = response.getJoinedText(); retryXoauthWithNewToken = XOAuth2ChallengeParser.shouldRetry(replyText, host); //Per Google spec, respond to challenge with empty response diff --git a/mail/protocols/smtp/src/main/java/com/fsck/k9/mail/transport/smtp/StatusCodeClass.java b/mail/protocols/smtp/src/main/java/com/fsck/k9/mail/transport/smtp/StatusCodeClass.java index 842863f190..197f4975e2 100644 --- a/mail/protocols/smtp/src/main/java/com/fsck/k9/mail/transport/smtp/StatusCodeClass.java +++ b/mail/protocols/smtp/src/main/java/com/fsck/k9/mail/transport/smtp/StatusCodeClass.java @@ -6,20 +6,8 @@ enum StatusCodeClass { PERSISTENT_TRANSIENT_FAILURE(4), PERMANENT_FAILURE(5); - final int codeClass; - - static StatusCodeClass parse(String statusCodeClassString) { - int value = Integer.parseInt(statusCodeClassString); - for (StatusCodeClass classEnum : StatusCodeClass.values()) { - if (classEnum.codeClass == value) { - return classEnum; - } - } - return null; - } - StatusCodeClass(int codeClass) { this.codeClass = codeClass; } diff --git a/mail/protocols/smtp/src/main/java/com/fsck/k9/mail/transport/smtp/StatusCodeDetail.java b/mail/protocols/smtp/src/main/java/com/fsck/k9/mail/transport/smtp/StatusCodeDetail.java deleted file mode 100644 index 2ea73fe3c6..0000000000 --- a/mail/protocols/smtp/src/main/java/com/fsck/k9/mail/transport/smtp/StatusCodeDetail.java +++ /dev/null @@ -1,80 +0,0 @@ -package com.fsck.k9.mail.transport.smtp; - - -enum StatusCodeDetail { - UNDEFINED(StatusCodeSubject.UNDEFINED, 0), - OTHER_ADDRESS_STATUS(StatusCodeSubject.ADDRESSING, 0), - BAD_DESTINATION_MAILBOX_ADDRESS(StatusCodeSubject.ADDRESSING, 1), - BAD_DESTINATION_SYSTEM_ADDRESS(StatusCodeSubject.ADDRESSING, 2), - BAD_DESTINATION_MAILBOX_ADDRESS_SYNTAX(StatusCodeSubject.ADDRESSING, 3), - DESTINATION_MAILBOX_ADDRESS_AMBIGUOUS(StatusCodeSubject.ADDRESSING, 4), - DESTINATION_ADDRESS_VALID(StatusCodeSubject.ADDRESSING, 5), - DESTINATION_MAILBOX_MOVED(StatusCodeSubject.ADDRESSING, 6), - BAD_SENDER_MAILBOX_SYNTAX(StatusCodeSubject.ADDRESSING, 7), - BAD_SENDER_SYSTEM_ADDRESS(StatusCodeSubject.ADDRESSING, 8), - - OTHER_MAILBOX_STATUS(StatusCodeSubject.MAILBOX, 0), - MAILBOX_DISABLED(StatusCodeSubject.MAILBOX, 1), - MAILBOX_FULL(StatusCodeSubject.MAILBOX, 2), - MESSAGE_LENGTH_EXCEEDED(StatusCodeSubject.MAILBOX, 3), - MAILING_LIST_EXPANSION_PROBLEM(StatusCodeSubject.MAILBOX, 4), - - OTHER_MAIL_SYSTEM_STATUS(StatusCodeSubject.MAIL_SYSTEM, 0), - MAIL_SYSTEM_FULL(StatusCodeSubject.MAIL_SYSTEM, 1), - SYSTEM_NOT_ACCEPTING_MESSAGES(StatusCodeSubject.MAIL_SYSTEM, 2), - SYSTEM_INCAPABLE_OF_FEATURE(StatusCodeSubject.MAIL_SYSTEM, 3), - MESSAGE_TOO_BIG(StatusCodeSubject.MAIL_SYSTEM, 4), - SYSTEM_INCORRECTLY_CONFIGURED(StatusCodeSubject.MAIL_SYSTEM, 5), - - OTHER_NETWORK_ROUTING(StatusCodeSubject.NETWORK_ROUTING, 0), - NO_ANSWER_FROM_HOST(StatusCodeSubject.NETWORK_ROUTING, 1), - BAD_CONNECTION(StatusCodeSubject.NETWORK_ROUTING, 2), - DIRECTORY_SERVER_FAILURE(StatusCodeSubject.NETWORK_ROUTING, 3), - UNABLE_TO_ROUTE(StatusCodeSubject.NETWORK_ROUTING, 4), - MAIL_SYSTEM_CONGESTION(StatusCodeSubject.NETWORK_ROUTING, 5), - ROUTING_LOOP_DETECTED(StatusCodeSubject.NETWORK_ROUTING, 6), - DELIVERY_TIME_EXPIRED(StatusCodeSubject.NETWORK_ROUTING, 7), - - OTHER_MAIL_DELIVERY_PROTOCOL(StatusCodeSubject.MAIL_DELIVERY_PROTOCOL, 0), - INVALID_COMMAND(StatusCodeSubject.MAIL_DELIVERY_PROTOCOL, 1), - SYNTAX_ERROR(StatusCodeSubject.MAIL_DELIVERY_PROTOCOL, 2), - TOO_MANY_RECIPIENTS(StatusCodeSubject.MAIL_DELIVERY_PROTOCOL, 3), - INVALID_COMMAND_ARGUMENTS(StatusCodeSubject.MAIL_DELIVERY_PROTOCOL, 4), - WRONG_PROTOCOL_VERSION(StatusCodeSubject.MAIL_DELIVERY_PROTOCOL, 5), - - OTHER_MESSAGE_CONTENT_OR_MEDIA(StatusCodeSubject.MESSAGE_CONTENT_OR_MEDIA, 0), - MEDIA_NOT_SUPPORTED(StatusCodeSubject.MESSAGE_CONTENT_OR_MEDIA, 1), - CONVERSION_REQUIRED_AND_PROHIBITED(StatusCodeSubject.MESSAGE_CONTENT_OR_MEDIA, 2), - CONVERSION_REQUIRED_BUT_UNSUPPORTED(StatusCodeSubject.MESSAGE_CONTENT_OR_MEDIA, 3), - CONVERSION_WITH_LOSS_PERFORMED(StatusCodeSubject.MESSAGE_CONTENT_OR_MEDIA, 4), - CONVERSION_FAILED(StatusCodeSubject.MESSAGE_CONTENT_OR_MEDIA, 5), - - OTHER_SECURITY_OR_POLICY_STATUS(StatusCodeSubject.SECURITY_OR_POLICY_STATUS, 0), - DELIVERY_NOT_AUTHORIZED(StatusCodeSubject.SECURITY_OR_POLICY_STATUS, 1), - MAILING_LIST_EXPANSION_PROHIBITED(StatusCodeSubject.SECURITY_OR_POLICY_STATUS, 2), - SECURITY_CONVERSION_REQUIRED(StatusCodeSubject.SECURITY_OR_POLICY_STATUS, 3), - SECURITY_FEATURES_UNSUPPORTED(StatusCodeSubject.SECURITY_OR_POLICY_STATUS, 4), - CRYPTOGRAPHIC_FAILURE(StatusCodeSubject.SECURITY_OR_POLICY_STATUS, 5), - CRYPTOGRAPHIC_ALGORITHM_UNSUPPORTED(StatusCodeSubject.SECURITY_OR_POLICY_STATUS, 6), - MESSAGE_INTEGRITY_FAILURE(StatusCodeSubject.SECURITY_OR_POLICY_STATUS, 7); - - - private final StatusCodeSubject subject; - private final int detail; - - - public static StatusCodeDetail parse(StatusCodeSubject statusCodeSubject, String statusCodeDetailString) { - int value = Integer.parseInt(statusCodeDetailString); - for (StatusCodeDetail detailEnum : StatusCodeDetail.values()) { - if (detailEnum.subject == statusCodeSubject && detailEnum.detail == value) { - return detailEnum; - } - } - return null; - } - - StatusCodeDetail(StatusCodeSubject subject, int detail) { - this.subject = subject; - this.detail = detail; - } -} diff --git a/mail/protocols/smtp/src/main/java/com/fsck/k9/mail/transport/smtp/StatusCodeSubject.java b/mail/protocols/smtp/src/main/java/com/fsck/k9/mail/transport/smtp/StatusCodeSubject.java deleted file mode 100644 index ff004f3068..0000000000 --- a/mail/protocols/smtp/src/main/java/com/fsck/k9/mail/transport/smtp/StatusCodeSubject.java +++ /dev/null @@ -1,31 +0,0 @@ -package com.fsck.k9.mail.transport.smtp; - - -enum StatusCodeSubject { - UNDEFINED(0), - ADDRESSING(1), - MAILBOX(2), - MAIL_SYSTEM(3), - NETWORK_ROUTING(4), - MAIL_DELIVERY_PROTOCOL(5), - MESSAGE_CONTENT_OR_MEDIA(6), - SECURITY_OR_POLICY_STATUS(7); - - - private final int codeSubject; - - - static StatusCodeSubject parse(String statusCodeSubjectString) { - int value = Integer.parseInt(statusCodeSubjectString); - for (StatusCodeSubject classEnum : StatusCodeSubject.values()) { - if (classEnum.codeSubject == value) { - return classEnum; - } - } - return null; - } - - StatusCodeSubject(int codeSubject) { - this.codeSubject = codeSubject; - } -} diff --git a/mail/protocols/smtp/src/test/java/com/fsck/k9/mail/transport/smtp/SmtpTransportTest.java b/mail/protocols/smtp/src/test/java/com/fsck/k9/mail/transport/smtp/SmtpTransportTest.java index 5e961d9f88..49e197b1e9 100644 --- a/mail/protocols/smtp/src/test/java/com/fsck/k9/mail/transport/smtp/SmtpTransportTest.java +++ b/mail/protocols/smtp/src/test/java/com/fsck/k9/mail/transport/smtp/SmtpTransportTest.java @@ -151,8 +151,8 @@ public class SmtpTransportTest { server.output("250-localhost Hello client.localhost"); server.output("250 AUTH CRAM-MD5"); server.expect("AUTH CRAM-MD5"); - server.output(Base64.encode("<24609.1047914046@localhost>")); - server.expect("dXNlciA3NmYxNWEzZmYwYTNiOGI1NzcxZmNhODZlNTcyMDk2Zg=="); + server.output("334 " + Base64.encode("<24609.1047914046@localhost>")); + server.expect("dXNlciAyZDBlNTcwYzZlYWI0ZjY3ZDUyZmFkN2Q1NGExZDJhYQ=="); server.output("235 2.7.0 Authentication successful"); SmtpTransport transport = startServerAndCreateSmtpTransport(server, AuthType.CRAM_MD5, ConnectionSecurity.NONE); @@ -442,8 +442,8 @@ public class SmtpTransportTest { server.output("250-localhost Hello client.localhost"); server.output("250 AUTH CRAM-MD5"); server.expect("AUTH CRAM-MD5"); - server.output(Base64.encode("<24609.1047914046@localhost>")); - server.expect("dXNlciA3NmYxNWEzZmYwYTNiOGI1NzcxZmNhODZlNTcyMDk2Zg=="); + server.output("334 " + Base64.encode("<24609.1047914046@localhost>")); + server.expect("dXNlciAyZDBlNTcwYzZlYWI0ZjY3ZDUyZmFkN2Q1NGExZDJhYQ=="); server.output("235 2.7.0 Authentication successful"); SmtpTransport transport = startServerAndCreateSmtpTransport(server, AuthType.AUTOMATIC, ConnectionSecurity.NONE); -- GitLab From 6da9195179d5ff255a23145472a86c88aa2ffc94 Mon Sep 17 00:00:00 2001 From: cketti Date: Sun, 27 Mar 2022 16:26:52 +0200 Subject: [PATCH 06/75] Truncate file when overwriting existing settings file --- .../src/main/java/com/fsck/k9/preferences/SettingsExporter.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/core/src/main/java/com/fsck/k9/preferences/SettingsExporter.kt b/app/core/src/main/java/com/fsck/k9/preferences/SettingsExporter.kt index 39983b1391..08bc5bbefc 100644 --- a/app/core/src/main/java/com/fsck/k9/preferences/SettingsExporter.kt +++ b/app/core/src/main/java/com/fsck/k9/preferences/SettingsExporter.kt @@ -33,7 +33,7 @@ class SettingsExporter( updateNotificationSettings(accountUuids) try { - contentResolver.openOutputStream(uri)!!.use { outputStream -> + contentResolver.openOutputStream(uri, "wt")!!.use { outputStream -> exportPreferences(outputStream, includeGlobals, accountUuids) } } catch (e: Exception) { -- GitLab From 31860a209e8e7fa36dbc2bbe9c589b09e28f448f Mon Sep 17 00:00:00 2001 From: cketti Date: Sun, 27 Mar 2022 22:55:45 +0200 Subject: [PATCH 07/75] Fix usage of `ContentResolver.openOutputStream()` --- app/core/src/main/java/com/fsck/k9/logging/LogFileWriter.kt | 2 +- .../test/java/com/fsck/k9/logging/LogcatLogFileWriterTest.kt | 4 ++-- .../java/com/fsck/k9/ui/messageview/AttachmentController.java | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/core/src/main/java/com/fsck/k9/logging/LogFileWriter.kt b/app/core/src/main/java/com/fsck/k9/logging/LogFileWriter.kt index d81c9e1661..831637708c 100644 --- a/app/core/src/main/java/com/fsck/k9/logging/LogFileWriter.kt +++ b/app/core/src/main/java/com/fsck/k9/logging/LogFileWriter.kt @@ -26,7 +26,7 @@ class LogcatLogFileWriter( private fun writeLogBlocking(contentUri: Uri) { Timber.v("Writing logcat output to content URI: %s", contentUri) - val outputStream = contentResolver.openOutputStream(contentUri) + val outputStream = contentResolver.openOutputStream(contentUri, "wt") ?: error("Error opening contentUri for writing") outputStream.use { diff --git a/app/core/src/test/java/com/fsck/k9/logging/LogcatLogFileWriterTest.kt b/app/core/src/test/java/com/fsck/k9/logging/LogcatLogFileWriterTest.kt index 2fcc6e93ff..a4229bbb0d 100644 --- a/app/core/src/test/java/com/fsck/k9/logging/LogcatLogFileWriterTest.kt +++ b/app/core/src/test/java/com/fsck/k9/logging/LogcatLogFileWriterTest.kt @@ -57,13 +57,13 @@ class LogcatLogFileWriterTest { private fun createContentResolver(): ContentResolver { return mock { - on { openOutputStream(contentUri) } doReturn outputStream + on { openOutputStream(contentUri, "wt") } doReturn outputStream } } private fun createThrowingContentResolver(exception: Exception): ContentResolver { return mock { - on { openOutputStream(contentUri) } doAnswer { throw exception } + on { openOutputStream(contentUri, "wt") } doAnswer { throw exception } } } diff --git a/app/ui/legacy/src/main/java/com/fsck/k9/ui/messageview/AttachmentController.java b/app/ui/legacy/src/main/java/com/fsck/k9/ui/messageview/AttachmentController.java index 1248e19e46..0d90de27b9 100644 --- a/app/ui/legacy/src/main/java/com/fsck/k9/ui/messageview/AttachmentController.java +++ b/app/ui/legacy/src/main/java/com/fsck/k9/ui/messageview/AttachmentController.java @@ -117,7 +117,7 @@ public class AttachmentController { ContentResolver contentResolver = context.getContentResolver(); InputStream in = contentResolver.openInputStream(attachment.internalUri); try { - OutputStream out = contentResolver.openOutputStream(documentUri); + OutputStream out = contentResolver.openOutputStream(documentUri, "wt"); try { IOUtils.copy(in, out); out.flush(); -- GitLab From 0a60b7ce21a4835429eb08186a6ad9f32482bbba Mon Sep 17 00:00:00 2001 From: cketti Date: Thu, 7 Apr 2022 05:03:19 +0200 Subject: [PATCH 08/75] Simplify UI to configure IMAP compression --- .../activity/setup/AccountSetupIncoming.java | 26 +++--------- .../res/layout/account_setup_incoming.xml | 40 ++----------------- app/ui/legacy/src/main/res/values/strings.xml | 5 +-- 3 files changed, 11 insertions(+), 60 deletions(-) diff --git a/app/ui/legacy/src/main/java/com/fsck/k9/activity/setup/AccountSetupIncoming.java b/app/ui/legacy/src/main/java/com/fsck/k9/activity/setup/AccountSetupIncoming.java index c3f27bcf59..05b98c668c 100644 --- a/app/ui/legacy/src/main/java/com/fsck/k9/activity/setup/AccountSetupIncoming.java +++ b/app/ui/legacy/src/main/java/com/fsck/k9/activity/setup/AccountSetupIncoming.java @@ -31,9 +31,7 @@ import com.fsck.k9.helper.EmailHelper; import com.fsck.k9.setup.ServerNameSuggester; import com.fsck.k9.ui.base.K9Activity; import com.fsck.k9.activity.setup.AccountSetupCheckSettings.CheckDirection; -import com.fsck.k9.controller.MessagingController; import com.fsck.k9.helper.Utility; -import com.fsck.k9.job.K9JobManager; import com.fsck.k9.mail.AuthType; import com.fsck.k9.mail.ConnectionSecurity; import com.fsck.k9.mail.MailServerDirection; @@ -63,8 +61,6 @@ public class AccountSetupIncoming extends K9Activity implements OnClickListener private static final String STATE_SECURITY_TYPE_POSITION = "stateSecurityTypePosition"; private static final String STATE_AUTH_TYPE_POSITION = "authTypePosition"; - private final MessagingController messagingController = DI.get(MessagingController.class); - private final K9JobManager jobManager = DI.get(K9JobManager.class); private final AccountCreator accountCreator = DI.get(AccountCreator.class); private final ServerNameSuggester serverNameSuggester = DI.get(ServerNameSuggester.class); @@ -89,9 +85,7 @@ public class AccountSetupIncoming extends K9Activity implements OnClickListener private Button mNextButton; private Account mAccount; private boolean mMakeDefault; - private CheckBox mCompressionMobile; - private CheckBox mCompressionWifi; - private CheckBox mCompressionOther; + private CheckBox useCompressionCheckBox; private CheckBox mSubscribedFoldersOnly; private AuthTypeAdapter mAuthTypeAdapter; private ConnectionSecurity[] mConnectionSecurityChoices = ConnectionSecurity.values(); @@ -138,9 +132,7 @@ public class AccountSetupIncoming extends K9Activity implements OnClickListener mWebdavAuthPathView = findViewById(R.id.webdav_auth_path); mWebdavMailboxPathView = findViewById(R.id.webdav_mailbox_path); mNextButton = findViewById(R.id.next); - mCompressionMobile = findViewById(R.id.compression_mobile); - mCompressionWifi = findViewById(R.id.compression_wifi); - mCompressionOther = findViewById(R.id.compression_other); + useCompressionCheckBox = findViewById(R.id.use_compression); mSubscribedFoldersOnly = findViewById(R.id.subscribed_folders_only); mAllowClientCertificateView = findViewById(R.id.account_allow_client_certificate); @@ -222,8 +214,7 @@ public class AccountSetupIncoming extends K9Activity implements OnClickListener findViewById(R.id.webdav_mailbox_alias_section).setVisibility(View.GONE); findViewById(R.id.webdav_owa_path_section).setVisibility(View.GONE); findViewById(R.id.webdav_auth_path_section).setVisibility(View.GONE); - findViewById(R.id.compression_section).setVisibility(View.GONE); - findViewById(R.id.compression_label).setVisibility(View.GONE); + useCompressionCheckBox.setVisibility(View.GONE); mSubscribedFoldersOnly.setVisibility(View.GONE); } else if (settings.type.equals(Protocols.IMAP)) { serverLayoutView.setHint(getString(R.string.account_setup_incoming_imap_server_label)); @@ -254,8 +245,7 @@ public class AccountSetupIncoming extends K9Activity implements OnClickListener findViewById(R.id.imap_path_prefix_section).setVisibility(View.GONE); findViewById(R.id.account_auth_type_label).setVisibility(View.GONE); findViewById(R.id.account_auth_type).setVisibility(View.GONE); - findViewById(R.id.compression_section).setVisibility(View.GONE); - findViewById(R.id.compression_label).setVisibility(View.GONE); + useCompressionCheckBox.setVisibility(View.GONE); mSubscribedFoldersOnly.setVisibility(View.GONE); String path = WebDavStoreSettings.getPath(settings); @@ -305,9 +295,7 @@ public class AccountSetupIncoming extends K9Activity implements OnClickListener updateAuthPlainTextFromSecurityType(settings.connectionSecurity); updateViewFromSecurity(); - mCompressionMobile.setChecked(mAccount.useCompression(NetworkType.MOBILE)); - mCompressionWifi.setChecked(mAccount.useCompression(NetworkType.WIFI)); - mCompressionOther.setChecked(mAccount.useCompression(NetworkType.OTHER)); + useCompressionCheckBox.setChecked(mAccount.useCompression(NetworkType.MOBILE)); if (settings.host != null) { mServerView.setText(settings.host); @@ -614,9 +602,7 @@ public class AccountSetupIncoming extends K9Activity implements OnClickListener mAccount.setIncomingServerSettings(settings); - mAccount.setCompression(NetworkType.MOBILE, mCompressionMobile.isChecked()); - mAccount.setCompression(NetworkType.WIFI, mCompressionWifi.isChecked()); - mAccount.setCompression(NetworkType.OTHER, mCompressionOther.isChecked()); + mAccount.setCompression(NetworkType.MOBILE, useCompressionCheckBox.isChecked()); mAccount.setSubscribedFoldersOnly(mSubscribedFoldersOnly.isChecked()); AccountSetupCheckSettings.actionCheckSettings(this, mAccount, CheckDirection.INCOMING); diff --git a/app/ui/legacy/src/main/res/layout/account_setup_incoming.xml b/app/ui/legacy/src/main/res/layout/account_setup_incoming.xml index 38bb5202d6..3807ce8c82 100644 --- a/app/ui/legacy/src/main/res/layout/account_setup_incoming.xml +++ b/app/ui/legacy/src/main/res/layout/account_setup_incoming.xml @@ -247,43 +247,11 @@ - - - - - - - - - - + android:layout_width="wrap_content" + android:text="@string/account_setup_incoming_use_compression" /> Delete from server Mark as read on server - Use compression on network: - Mobile - Wi-Fi - Other + Use compression Erase deleted messages on server Immediately -- GitLab From 903d0f43c4a584f4f70da2c81269ce033e05cfa3 Mon Sep 17 00:00:00 2001 From: cketti Date: Thu, 7 Apr 2022 05:12:19 +0200 Subject: [PATCH 09/75] Change `Account` to use a single setting for IMAP compression --- app/core/src/main/java/com/fsck/k9/Account.kt | 21 ++++--------------- .../fsck/k9/AccountPreferenceSerializer.kt | 18 +++------------- .../fsck/k9/backends/ImapBackendFactory.kt | 2 +- .../activity/setup/AccountSetupIncoming.java | 5 ++--- 4 files changed, 10 insertions(+), 36 deletions(-) diff --git a/app/core/src/main/java/com/fsck/k9/Account.kt b/app/core/src/main/java/com/fsck/k9/Account.kt index c1e66a2819..2195e1fceb 100644 --- a/app/core/src/main/java/com/fsck/k9/Account.kt +++ b/app/core/src/main/java/com/fsck/k9/Account.kt @@ -2,11 +2,9 @@ package com.fsck.k9 import com.fsck.k9.backend.api.SyncConfig.ExpungePolicy import com.fsck.k9.mail.Address -import com.fsck.k9.mail.NetworkType import com.fsck.k9.mail.ServerSettings import java.util.Calendar import java.util.Date -import java.util.concurrent.ConcurrentHashMap /** * Account stores all of the settings for a single account defined by the user. Each account is defined by a UUID. @@ -225,7 +223,10 @@ class Account(override val uuid: String) : BaseAccount { @set:Synchronized var idleRefreshMinutes = 0 - private val compressionMap: MutableMap = ConcurrentHashMap() + @get:JvmName("useCompression") + @get:Synchronized + @set:Synchronized + var useCompression = true @get:Synchronized @set:Synchronized @@ -500,20 +501,6 @@ class Account(override val uuid: String) : BaseAccount { this.sortAscending[sortType] = sortAscending } - @Synchronized - fun setCompression(networkType: NetworkType, useCompression: Boolean) { - compressionMap[networkType] = useCompression - } - - @Synchronized - fun useCompression(networkType: NetworkType): Boolean { - return compressionMap[networkType] ?: return true - } - - fun getCompressionMap(): Map { - return compressionMap.toMap() - } - @Synchronized fun replaceIdentities(identities: List) { this.identities = identities.toMutableList() diff --git a/app/core/src/main/java/com/fsck/k9/AccountPreferenceSerializer.kt b/app/core/src/main/java/com/fsck/k9/AccountPreferenceSerializer.kt index c881cb312d..3bb834d7b1 100644 --- a/app/core/src/main/java/com/fsck/k9/AccountPreferenceSerializer.kt +++ b/app/core/src/main/java/com/fsck/k9/AccountPreferenceSerializer.kt @@ -15,7 +15,6 @@ import com.fsck.k9.Account.ShowPictures import com.fsck.k9.Account.SortType import com.fsck.k9.Account.SpecialFolderSelection import com.fsck.k9.helper.Utility -import com.fsck.k9.mail.NetworkType import com.fsck.k9.mailstore.StorageManager import com.fsck.k9.preferences.Storage import com.fsck.k9.preferences.StorageEditor @@ -120,10 +119,7 @@ class AccountPreferenceSerializer( isDefaultQuotedTextShown = storage.getBoolean("$accountUuid.defaultQuotedTextShown", DEFAULT_QUOTED_TEXT_SHOWN) isReplyAfterQuote = storage.getBoolean("$accountUuid.replyAfterQuote", DEFAULT_REPLY_AFTER_QUOTE) isStripSignature = storage.getBoolean("$accountUuid.stripSignature", DEFAULT_STRIP_SIGNATURE) - for (type in NetworkType.values()) { - val useCompression = storage.getBoolean("$accountUuid.useCompression.$type", true) - setCompression(type, useCompression) - } + useCompression = storage.getBoolean("$accountUuid.useCompression", true) importedAutoExpandFolder = storage.getString("$accountUuid.autoExpandFolderName", null) @@ -334,13 +330,7 @@ class AccountPreferenceSerializer( editor.putLong("$accountUuid.lastFolderListRefreshTime", lastFolderListRefreshTime) editor.putBoolean("$accountUuid.isFinishedSetup", isFinishedSetup) - val compressionMap = getCompressionMap() - for (type in NetworkType.values()) { - val useCompression = compressionMap[type] - if (useCompression != null) { - editor.putBoolean("$accountUuid.useCompression.$type", useCompression) - } - } + editor.putBoolean("$accountUuid.useCompression", useCompression) } saveIdentities(account, storage, editor) @@ -456,10 +446,8 @@ class AccountPreferenceSerializer( editor.remove("$accountUuid.lastSyncTime") editor.remove("$accountUuid.lastFolderListRefreshTime") editor.remove("$accountUuid.isFinishedSetup") + editor.remove("$accountUuid.useCompression") - for (type in NetworkType.values()) { - editor.remove("$accountUuid.useCompression." + type.name) - } deleteIdentities(account, storage, editor) // TODO: Remove preference settings that may exist for individual folders in the account. } diff --git a/app/k9mail/src/main/java/com/fsck/k9/backends/ImapBackendFactory.kt b/app/k9mail/src/main/java/com/fsck/k9/backends/ImapBackendFactory.kt index 249eaa40b0..35ab04af41 100644 --- a/app/k9mail/src/main/java/com/fsck/k9/backends/ImapBackendFactory.kt +++ b/app/k9mail/src/main/java/com/fsck/k9/backends/ImapBackendFactory.kt @@ -66,7 +66,7 @@ class ImapBackendFactory( override fun isSubscribedFoldersOnly() = account.isSubscribedFoldersOnly - override fun useCompression(type: NetworkType) = account.useCompression(type) + override fun useCompression(type: NetworkType) = account.useCompression } } diff --git a/app/ui/legacy/src/main/java/com/fsck/k9/activity/setup/AccountSetupIncoming.java b/app/ui/legacy/src/main/java/com/fsck/k9/activity/setup/AccountSetupIncoming.java index 05b98c668c..16decb409d 100644 --- a/app/ui/legacy/src/main/java/com/fsck/k9/activity/setup/AccountSetupIncoming.java +++ b/app/ui/legacy/src/main/java/com/fsck/k9/activity/setup/AccountSetupIncoming.java @@ -35,7 +35,6 @@ import com.fsck.k9.helper.Utility; import com.fsck.k9.mail.AuthType; import com.fsck.k9.mail.ConnectionSecurity; import com.fsck.k9.mail.MailServerDirection; -import com.fsck.k9.mail.NetworkType; import com.fsck.k9.mail.ServerSettings; import com.fsck.k9.mail.store.imap.ImapStoreSettings; import com.fsck.k9.mail.store.webdav.WebDavStoreSettings; @@ -295,7 +294,7 @@ public class AccountSetupIncoming extends K9Activity implements OnClickListener updateAuthPlainTextFromSecurityType(settings.connectionSecurity); updateViewFromSecurity(); - useCompressionCheckBox.setChecked(mAccount.useCompression(NetworkType.MOBILE)); + useCompressionCheckBox.setChecked(mAccount.useCompression()); if (settings.host != null) { mServerView.setText(settings.host); @@ -602,7 +601,7 @@ public class AccountSetupIncoming extends K9Activity implements OnClickListener mAccount.setIncomingServerSettings(settings); - mAccount.setCompression(NetworkType.MOBILE, useCompressionCheckBox.isChecked()); + mAccount.setUseCompression(useCompressionCheckBox.isChecked()); mAccount.setSubscribedFoldersOnly(mSubscribedFoldersOnly.isChecked()); AccountSetupCheckSettings.actionCheckSettings(this, mAccount, CheckDirection.INCOMING); -- GitLab From a796b1d941f5b4d9b9b7fe12aa602f175b1a4aab Mon Sep 17 00:00:00 2001 From: cketti Date: Thu, 7 Apr 2022 05:15:37 +0200 Subject: [PATCH 10/75] Change IMAP code to use single compression setting --- .../fsck/k9/backends/ImapBackendFactory.kt | 3 +-- .../java/com/fsck/k9/mail/NetworkType.java | 25 ------------------- .../fsck/k9/mail/store/imap/ImapSettings.java | 4 +-- .../k9/mail/store/imap/ImapStoreConfig.kt | 4 +-- .../mail/store/imap/RealImapConnection.java | 25 +------------------ .../k9/mail/store/imap/RealImapStore.java | 5 ++-- .../mail/store/imap/SimpleImapSettings.java | 3 +-- 7 files changed, 8 insertions(+), 61 deletions(-) delete mode 100644 mail/common/src/main/java/com/fsck/k9/mail/NetworkType.java diff --git a/app/k9mail/src/main/java/com/fsck/k9/backends/ImapBackendFactory.kt b/app/k9mail/src/main/java/com/fsck/k9/backends/ImapBackendFactory.kt index 35ab04af41..bae731beb9 100644 --- a/app/k9mail/src/main/java/com/fsck/k9/backends/ImapBackendFactory.kt +++ b/app/k9mail/src/main/java/com/fsck/k9/backends/ImapBackendFactory.kt @@ -7,7 +7,6 @@ import com.fsck.k9.backend.BackendFactory import com.fsck.k9.backend.api.Backend import com.fsck.k9.backend.imap.ImapBackend import com.fsck.k9.backend.imap.ImapPushConfigProvider -import com.fsck.k9.mail.NetworkType import com.fsck.k9.mail.oauth.OAuth2TokenProvider import com.fsck.k9.mail.power.PowerManager import com.fsck.k9.mail.ssl.TrustedSocketFactory @@ -66,7 +65,7 @@ class ImapBackendFactory( override fun isSubscribedFoldersOnly() = account.isSubscribedFoldersOnly - override fun useCompression(type: NetworkType) = account.useCompression + override fun useCompression() = account.useCompression } } diff --git a/mail/common/src/main/java/com/fsck/k9/mail/NetworkType.java b/mail/common/src/main/java/com/fsck/k9/mail/NetworkType.java deleted file mode 100644 index 769858213c..0000000000 --- a/mail/common/src/main/java/com/fsck/k9/mail/NetworkType.java +++ /dev/null @@ -1,25 +0,0 @@ -package com.fsck.k9.mail; - -import android.net.ConnectivityManager; - -/** - * Enum for some of - * https://developer.android.com/reference/android/net/ConnectivityManager.html#TYPE_MOBILE etc. - */ -public enum NetworkType { - - WIFI, - MOBILE, - OTHER; - - public static NetworkType fromConnectivityManagerType(int type){ - switch (type) { - case ConnectivityManager.TYPE_MOBILE: - return MOBILE; - case ConnectivityManager.TYPE_WIFI: - return WIFI; - default: - return OTHER; - } - } -} diff --git a/mail/protocols/imap/src/main/java/com/fsck/k9/mail/store/imap/ImapSettings.java b/mail/protocols/imap/src/main/java/com/fsck/k9/mail/store/imap/ImapSettings.java index 4689a961af..d5c5e1efba 100644 --- a/mail/protocols/imap/src/main/java/com/fsck/k9/mail/store/imap/ImapSettings.java +++ b/mail/protocols/imap/src/main/java/com/fsck/k9/mail/store/imap/ImapSettings.java @@ -2,7 +2,7 @@ package com.fsck.k9.mail.store.imap; import com.fsck.k9.mail.AuthType; import com.fsck.k9.mail.ConnectionSecurity; -import com.fsck.k9.mail.NetworkType; + /** * Settings source for IMAP. Implemented in order to remove coupling between {@link ImapStore} and {@link ImapConnection}. @@ -22,7 +22,7 @@ interface ImapSettings { String getClientCertificateAlias(); - boolean useCompression(NetworkType type); + boolean useCompression(); String getPathPrefix(); diff --git a/mail/protocols/imap/src/main/java/com/fsck/k9/mail/store/imap/ImapStoreConfig.kt b/mail/protocols/imap/src/main/java/com/fsck/k9/mail/store/imap/ImapStoreConfig.kt index fef1512222..0bfbd58eeb 100644 --- a/mail/protocols/imap/src/main/java/com/fsck/k9/mail/store/imap/ImapStoreConfig.kt +++ b/mail/protocols/imap/src/main/java/com/fsck/k9/mail/store/imap/ImapStoreConfig.kt @@ -1,9 +1,7 @@ package com.fsck.k9.mail.store.imap -import com.fsck.k9.mail.NetworkType - interface ImapStoreConfig { val logLabel: String fun isSubscribedFoldersOnly(): Boolean - fun useCompression(type: NetworkType): Boolean + fun useCompression(): Boolean } diff --git a/mail/protocols/imap/src/main/java/com/fsck/k9/mail/store/imap/RealImapConnection.java b/mail/protocols/imap/src/main/java/com/fsck/k9/mail/store/imap/RealImapConnection.java index 6c5123c7e4..4679ffad1c 100644 --- a/mail/protocols/imap/src/main/java/com/fsck/k9/mail/store/imap/RealImapConnection.java +++ b/mail/protocols/imap/src/main/java/com/fsck/k9/mail/store/imap/RealImapConnection.java @@ -28,7 +28,6 @@ import java.util.zip.Inflater; import java.util.zip.InflaterInputStream; import android.net.ConnectivityManager; -import android.net.NetworkInfo; import com.fsck.k9.mail.Authentication; import com.fsck.k9.mail.AuthenticationFailedException; @@ -36,7 +35,6 @@ import com.fsck.k9.mail.CertificateValidationException; import com.fsck.k9.mail.ConnectionSecurity; import com.fsck.k9.mail.K9MailLib; import com.fsck.k9.mail.MessagingException; -import com.fsck.k9.mail.NetworkType; import com.fsck.k9.mail.filter.Base64; import com.fsck.k9.mail.filter.PeekableInputStream; import com.fsck.k9.mail.oauth.OAuth2TokenProvider; @@ -577,32 +575,11 @@ class RealImapConnection implements ImapConnection { } private void enableCompressionIfRequested() throws IOException, MessagingException { - if (hasCapability(Capabilities.COMPRESS_DEFLATE) && shouldEnableCompression()) { + if (hasCapability(Capabilities.COMPRESS_DEFLATE) && settings.useCompression()) { enableCompression(); } } - private boolean shouldEnableCompression() { - boolean useCompression = true; - - NetworkInfo networkInfo = connectivityManager.getActiveNetworkInfo(); - if (networkInfo != null) { - int type = networkInfo.getType(); - if (K9MailLib.isDebug()) { - Timber.d("On network type %s", type); - } - - NetworkType networkType = NetworkType.fromConnectivityManagerType(type); - useCompression = settings.useCompression(networkType); - } - - if (K9MailLib.isDebug()) { - Timber.d("useCompression: %b", useCompression); - } - - return useCompression; - } - private void enableCompression() throws IOException, MessagingException { try { executeSimpleCommand(Commands.COMPRESS_DEFLATE); diff --git a/mail/protocols/imap/src/main/java/com/fsck/k9/mail/store/imap/RealImapStore.java b/mail/protocols/imap/src/main/java/com/fsck/k9/mail/store/imap/RealImapStore.java index 8b8e24fa0d..42907dc840 100644 --- a/mail/protocols/imap/src/main/java/com/fsck/k9/mail/store/imap/RealImapStore.java +++ b/mail/protocols/imap/src/main/java/com/fsck/k9/mail/store/imap/RealImapStore.java @@ -21,7 +21,6 @@ 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.NetworkType; import com.fsck.k9.mail.ServerSettings; import com.fsck.k9.mail.oauth.OAuth2TokenProvider; import com.fsck.k9.mail.ssl.TrustedSocketFactory; @@ -382,8 +381,8 @@ class RealImapStore implements ImapStore, ImapConnectionManager, InternalImapSto } @Override - public boolean useCompression(final NetworkType type) { - return config.useCompression(type); + public boolean useCompression() { + return config.useCompression(); } @Override diff --git a/mail/protocols/imap/src/test/java/com/fsck/k9/mail/store/imap/SimpleImapSettings.java b/mail/protocols/imap/src/test/java/com/fsck/k9/mail/store/imap/SimpleImapSettings.java index e14d09b9d8..391ae35d8a 100644 --- a/mail/protocols/imap/src/test/java/com/fsck/k9/mail/store/imap/SimpleImapSettings.java +++ b/mail/protocols/imap/src/test/java/com/fsck/k9/mail/store/imap/SimpleImapSettings.java @@ -3,7 +3,6 @@ package com.fsck.k9.mail.store.imap; import com.fsck.k9.mail.AuthType; import com.fsck.k9.mail.ConnectionSecurity; -import com.fsck.k9.mail.NetworkType; class SimpleImapSettings implements ImapSettings { @@ -55,7 +54,7 @@ class SimpleImapSettings implements ImapSettings { } @Override - public boolean useCompression(NetworkType type) { + public boolean useCompression() { return useCompression; } -- GitLab From 7be4ea62fe68a468fd7ffcae340530d3ae74226b Mon Sep 17 00:00:00 2001 From: cketti Date: Thu, 7 Apr 2022 05:25:29 +0200 Subject: [PATCH 11/75] Remove `ImapStore`'s dependency on `ConnectivityManager` --- .../fsck/k9/backends/ImapBackendFactory.kt | 4 ---- .../java/com/fsck/k9/backends/KoinModule.kt | 1 - .../com/fsck/k9/mail/store/imap/ImapStore.kt | 4 +--- .../mail/store/imap/RealImapConnection.java | 10 ++------- .../k9/mail/store/imap/RealImapStore.java | 7 +----- .../store/imap/RealImapConnectionTest.java | 22 ++++++------------- .../k9/mail/store/imap/RealImapStoreTest.java | 10 +++------ 7 files changed, 14 insertions(+), 44 deletions(-) diff --git a/app/k9mail/src/main/java/com/fsck/k9/backends/ImapBackendFactory.kt b/app/k9mail/src/main/java/com/fsck/k9/backends/ImapBackendFactory.kt index bae731beb9..989f428655 100644 --- a/app/k9mail/src/main/java/com/fsck/k9/backends/ImapBackendFactory.kt +++ b/app/k9mail/src/main/java/com/fsck/k9/backends/ImapBackendFactory.kt @@ -1,7 +1,5 @@ package com.fsck.k9.backends -import android.content.Context -import android.net.ConnectivityManager import com.fsck.k9.Account import com.fsck.k9.backend.BackendFactory import com.fsck.k9.backend.api.Backend @@ -21,7 +19,6 @@ import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.map class ImapBackendFactory( - private val context: Context, private val accountManager: AccountManager, private val powerManager: PowerManager, private val idleRefreshManager: IdleRefreshManager, @@ -53,7 +50,6 @@ class ImapBackendFactory( account.incomingServerSettings, config, trustedSocketFactory, - context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager, oAuth2TokenProvider ) } diff --git a/app/k9mail/src/main/java/com/fsck/k9/backends/KoinModule.kt b/app/k9mail/src/main/java/com/fsck/k9/backends/KoinModule.kt index 13d64f39ee..8dd9438a3c 100644 --- a/app/k9mail/src/main/java/com/fsck/k9/backends/KoinModule.kt +++ b/app/k9mail/src/main/java/com/fsck/k9/backends/KoinModule.kt @@ -20,7 +20,6 @@ val backendsModule = module { } single { ImapBackendFactory( - context = get(), accountManager = get(), powerManager = get(), idleRefreshManager = get(), diff --git a/mail/protocols/imap/src/main/java/com/fsck/k9/mail/store/imap/ImapStore.kt b/mail/protocols/imap/src/main/java/com/fsck/k9/mail/store/imap/ImapStore.kt index 1cba6012dc..bcb484c6a2 100644 --- a/mail/protocols/imap/src/main/java/com/fsck/k9/mail/store/imap/ImapStore.kt +++ b/mail/protocols/imap/src/main/java/com/fsck/k9/mail/store/imap/ImapStore.kt @@ -1,6 +1,5 @@ package com.fsck.k9.mail.store.imap -import android.net.ConnectivityManager import com.fsck.k9.mail.MessagingException import com.fsck.k9.mail.ServerSettings import com.fsck.k9.mail.oauth.OAuth2TokenProvider @@ -22,10 +21,9 @@ interface ImapStore { serverSettings: ServerSettings, config: ImapStoreConfig, trustedSocketFactory: TrustedSocketFactory, - connectivityManager: ConnectivityManager, oauthTokenProvider: OAuth2TokenProvider? ): ImapStore { - return RealImapStore(serverSettings, config, trustedSocketFactory, connectivityManager, oauthTokenProvider) + return RealImapStore(serverSettings, config, trustedSocketFactory, oauthTokenProvider) } } } diff --git a/mail/protocols/imap/src/main/java/com/fsck/k9/mail/store/imap/RealImapConnection.java b/mail/protocols/imap/src/main/java/com/fsck/k9/mail/store/imap/RealImapConnection.java index 4679ffad1c..b9f8095e64 100644 --- a/mail/protocols/imap/src/main/java/com/fsck/k9/mail/store/imap/RealImapConnection.java +++ b/mail/protocols/imap/src/main/java/com/fsck/k9/mail/store/imap/RealImapConnection.java @@ -27,8 +27,6 @@ import java.util.regex.Pattern; import java.util.zip.Inflater; import java.util.zip.InflaterInputStream; -import android.net.ConnectivityManager; - import com.fsck.k9.mail.Authentication; import com.fsck.k9.mail.AuthenticationFailedException; import com.fsck.k9.mail.CertificateValidationException; @@ -75,7 +73,6 @@ class RealImapConnection implements ImapConnection { private static final int LENGTH_LIMIT_WITH_CONDSTORE = 8172; - private final ConnectivityManager connectivityManager; private final OAuth2TokenProvider oauthTokenProvider; private final TrustedSocketFactory socketFactory; private final int socketConnectTimeout; @@ -95,10 +92,9 @@ class RealImapConnection implements ImapConnection { public RealImapConnection(ImapSettings settings, TrustedSocketFactory socketFactory, - ConnectivityManager connectivityManager, OAuth2TokenProvider oauthTokenProvider, int connectionGeneration) { + OAuth2TokenProvider oauthTokenProvider, int connectionGeneration) { this.settings = settings; this.socketFactory = socketFactory; - this.connectivityManager = connectivityManager; this.oauthTokenProvider = oauthTokenProvider; this.socketConnectTimeout = SOCKET_CONNECT_TIMEOUT; this.socketReadTimeout = SOCKET_READ_TIMEOUT; @@ -106,12 +102,10 @@ class RealImapConnection implements ImapConnection { } public RealImapConnection(ImapSettings settings, TrustedSocketFactory socketFactory, - ConnectivityManager connectivityManager, OAuth2TokenProvider oauthTokenProvider, - int socketConnectTimeout, int socketReadTimeout, + OAuth2TokenProvider oauthTokenProvider, int socketConnectTimeout, int socketReadTimeout, int connectionGeneration) { this.settings = settings; this.socketFactory = socketFactory; - this.connectivityManager = connectivityManager; this.oauthTokenProvider = oauthTokenProvider; this.socketConnectTimeout = socketConnectTimeout; this.socketReadTimeout = socketReadTimeout; diff --git a/mail/protocols/imap/src/main/java/com/fsck/k9/mail/store/imap/RealImapStore.java b/mail/protocols/imap/src/main/java/com/fsck/k9/mail/store/imap/RealImapStore.java index 42907dc840..3011442f73 100644 --- a/mail/protocols/imap/src/main/java/com/fsck/k9/mail/store/imap/RealImapStore.java +++ b/mail/protocols/imap/src/main/java/com/fsck/k9/mail/store/imap/RealImapStore.java @@ -13,7 +13,6 @@ import java.util.List; import java.util.Map; import java.util.Set; -import android.net.ConnectivityManager; import androidx.annotation.Nullable; import com.fsck.k9.mail.AuthType; @@ -37,7 +36,6 @@ class RealImapStore implements ImapStore, ImapConnectionManager, InternalImapSto private final ImapStoreConfig config; private final TrustedSocketFactory trustedSocketFactory; private Set permanentFlagsIndex = EnumSet.noneOf(Flag.class); - private ConnectivityManager connectivityManager; private OAuth2TokenProvider oauthTokenProvider; private String host; @@ -56,8 +54,7 @@ class RealImapStore implements ImapStore, ImapConnectionManager, InternalImapSto public RealImapStore(ServerSettings serverSettings, ImapStoreConfig config, - TrustedSocketFactory trustedSocketFactory, ConnectivityManager connectivityManager, - OAuth2TokenProvider oauthTokenProvider) { + TrustedSocketFactory trustedSocketFactory, OAuth2TokenProvider oauthTokenProvider) { this.config = config; this.trustedSocketFactory = trustedSocketFactory; @@ -65,7 +62,6 @@ class RealImapStore implements ImapStore, ImapConnectionManager, InternalImapSto port = serverSettings.port; connectionSecurity = serverSettings.connectionSecurity; - this.connectivityManager = connectivityManager; this.oauthTokenProvider = oauthTokenProvider; authType = serverSettings.authenticationType; @@ -326,7 +322,6 @@ class RealImapStore implements ImapStore, ImapConnectionManager, InternalImapSto return new RealImapConnection( new StoreImapSettings(), trustedSocketFactory, - connectivityManager, oauthTokenProvider, connectionGeneration); } diff --git a/mail/protocols/imap/src/test/java/com/fsck/k9/mail/store/imap/RealImapConnectionTest.java b/mail/protocols/imap/src/test/java/com/fsck/k9/mail/store/imap/RealImapConnectionTest.java index 9232c9df3e..cbfd5f2a52 100644 --- a/mail/protocols/imap/src/test/java/com/fsck/k9/mail/store/imap/RealImapConnectionTest.java +++ b/mail/protocols/imap/src/test/java/com/fsck/k9/mail/store/imap/RealImapConnectionTest.java @@ -6,7 +6,6 @@ import java.net.UnknownHostException; import java.util.List; import android.app.Activity; -import android.net.ConnectivityManager; import com.fsck.k9.mail.AuthType; import com.fsck.k9.mail.AuthenticationFailedException; @@ -33,7 +32,6 @@ import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertThat; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; -import static org.mockito.Mockito.mock; @RunWith(K9LibRobolectricTestRunner.class) @@ -53,14 +51,12 @@ public class RealImapConnectionTest { private TrustedSocketFactory socketFactory; - private ConnectivityManager connectivityManager; private OAuth2TokenProvider oAuth2TokenProvider; private SimpleImapSettings settings; @Before public void setUp() throws Exception { - connectivityManager = mock(ConnectivityManager.class); oAuth2TokenProvider = createOAuth2TokenProvider(); socketFactory = TestTrustedSocketFactory.newInstance(); @@ -619,8 +615,7 @@ public class RealImapConnectionTest { public void open_withConnectionError_shouldThrow() throws Exception { settings.setHost("127.1.2.3"); settings.setPort(143); - ImapConnection imapConnection = createImapConnection( - settings, socketFactory, connectivityManager, oAuth2TokenProvider); + ImapConnection imapConnection = createImapConnection(settings, socketFactory, oAuth2TokenProvider); try { imapConnection.open(); @@ -635,8 +630,7 @@ public class RealImapConnectionTest { public void open_withInvalidHostname_shouldThrow() throws Exception { settings.setHost("host name"); settings.setPort(143); - ImapConnection imapConnection = createImapConnection( - settings, socketFactory, connectivityManager, oAuth2TokenProvider); + ImapConnection imapConnection = createImapConnection(settings, socketFactory, oAuth2TokenProvider); try { imapConnection.open(); @@ -826,8 +820,7 @@ public class RealImapConnectionTest { @Test public void isConnected_withoutPreviousOpen_shouldReturnFalse() throws Exception { - ImapConnection imapConnection = createImapConnection( - settings, socketFactory, connectivityManager, oAuth2TokenProvider); + ImapConnection imapConnection = createImapConnection(settings, socketFactory, oAuth2TokenProvider); boolean result = imapConnection.isConnected(); @@ -863,8 +856,7 @@ public class RealImapConnectionTest { @Test public void close_withoutOpen_shouldNotThrow() throws Exception { - ImapConnection imapConnection = createImapConnection( - settings, socketFactory, connectivityManager, oAuth2TokenProvider); + ImapConnection imapConnection = createImapConnection(settings, socketFactory, oAuth2TokenProvider); imapConnection.close(); } @@ -973,8 +965,8 @@ public class RealImapConnectionTest { } private ImapConnection createImapConnection(ImapSettings settings, TrustedSocketFactory socketFactory, - ConnectivityManager connectivityManager, OAuth2TokenProvider oAuth2TokenProvider) { - return new RealImapConnection(settings, socketFactory, connectivityManager, oAuth2TokenProvider, + OAuth2TokenProvider oAuth2TokenProvider) { + return new RealImapConnection(settings, socketFactory, oAuth2TokenProvider, SOCKET_CONNECT_TIMEOUT, SOCKET_READ_TIMEOUT, 1); } @@ -982,7 +974,7 @@ public class RealImapConnectionTest { server.start(); settings.setHost(server.getHost()); settings.setPort(server.getPort()); - return createImapConnection(settings, socketFactory, connectivityManager, oAuth2TokenProvider); + return createImapConnection(settings, socketFactory, oAuth2TokenProvider); } private ImapConnection simpleOpen(MockImapServer server) throws Exception { diff --git a/mail/protocols/imap/src/test/java/com/fsck/k9/mail/store/imap/RealImapStoreTest.java b/mail/protocols/imap/src/test/java/com/fsck/k9/mail/store/imap/RealImapStoreTest.java index adc69fc5ac..ef95cf5810 100644 --- a/mail/protocols/imap/src/test/java/com/fsck/k9/mail/store/imap/RealImapStoreTest.java +++ b/mail/protocols/imap/src/test/java/com/fsck/k9/mail/store/imap/RealImapStoreTest.java @@ -12,8 +12,6 @@ import java.util.List; import java.util.Map; import java.util.Set; -import android.net.ConnectivityManager; - import com.fsck.k9.mail.AuthType; import com.fsck.k9.mail.ConnectionSecurity; import com.fsck.k9.mail.FolderType; @@ -47,10 +45,9 @@ public class RealImapStoreTest { public void setUp() throws Exception { ServerSettings serverSettings = createServerSettings(); TrustedSocketFactory trustedSocketFactory = mock(TrustedSocketFactory.class); - ConnectivityManager connectivityManager = mock(ConnectivityManager.class); OAuth2TokenProvider oauth2TokenProvider = mock(OAuth2TokenProvider.class); - imapStore = new TestImapStore(serverSettings, config, trustedSocketFactory, connectivityManager, + imapStore = new TestImapStore(serverSettings, config, trustedSocketFactory, oauth2TokenProvider); } @@ -463,9 +460,8 @@ public class RealImapStoreTest { private String testCombinedPrefix; public TestImapStore(ServerSettings serverSettings, ImapStoreConfig config, - TrustedSocketFactory trustedSocketFactory, ConnectivityManager connectivityManager, - OAuth2TokenProvider oauth2TokenProvider) { - super(serverSettings, config, trustedSocketFactory, connectivityManager, oauth2TokenProvider); + TrustedSocketFactory trustedSocketFactory, OAuth2TokenProvider oauth2TokenProvider) { + super(serverSettings, config, trustedSocketFactory, oauth2TokenProvider); } @Override -- GitLab From 814583a89e4f9cd84ae326f52d67a797532d967e Mon Sep 17 00:00:00 2001 From: cketti Date: Thu, 7 Apr 2022 05:36:14 +0200 Subject: [PATCH 12/75] Add migration for persisted IMAP compression preference --- .../k9/preferences/K9StoragePersister.java | 2 +- .../migrations/StorageMigrationTo18.kt | 36 +++++++++++++++++++ .../migrations/StorageMigrations.kt | 1 + 3 files changed, 38 insertions(+), 1 deletion(-) create mode 100644 app/storage/src/main/java/com/fsck/k9/preferences/migrations/StorageMigrationTo18.kt diff --git a/app/storage/src/main/java/com/fsck/k9/preferences/K9StoragePersister.java b/app/storage/src/main/java/com/fsck/k9/preferences/K9StoragePersister.java index 0a0c290b80..6ea08aad68 100644 --- a/app/storage/src/main/java/com/fsck/k9/preferences/K9StoragePersister.java +++ b/app/storage/src/main/java/com/fsck/k9/preferences/K9StoragePersister.java @@ -21,7 +21,7 @@ import timber.log.Timber; public class K9StoragePersister implements StoragePersister { - private static final int DB_VERSION = 17; + private static final int DB_VERSION = 18; private static final String DB_NAME = "preferences_storage"; private final Context context; diff --git a/app/storage/src/main/java/com/fsck/k9/preferences/migrations/StorageMigrationTo18.kt b/app/storage/src/main/java/com/fsck/k9/preferences/migrations/StorageMigrationTo18.kt new file mode 100644 index 0000000000..15a98f9521 --- /dev/null +++ b/app/storage/src/main/java/com/fsck/k9/preferences/migrations/StorageMigrationTo18.kt @@ -0,0 +1,36 @@ +package com.fsck.k9.preferences.migrations + +import android.database.sqlite.SQLiteDatabase + +/** + * Rewrite the per-network type IMAP compression settings to a single setting. + */ +class StorageMigrationTo18( + private val db: SQLiteDatabase, + private val migrationsHelper: StorageMigrationsHelper +) { + fun rewriteImapCompressionSettings() { + val accountUuidsListValue = migrationsHelper.readValue(db, "accountUuids") + if (accountUuidsListValue == null || accountUuidsListValue.isEmpty()) { + return + } + + val accountUuids = accountUuidsListValue.split(",") + for (accountUuid in accountUuids) { + rewriteImapCompressionSetting(accountUuid) + } + } + + private fun rewriteImapCompressionSetting(accountUuid: String) { + val useCompressionWifi = migrationsHelper.readValue(db, "$accountUuid.useCompression.WIFI").toBoolean() + val useCompressionMobile = migrationsHelper.readValue(db, "$accountUuid.useCompression.MOBILE").toBoolean() + val useCompressionOther = migrationsHelper.readValue(db, "$accountUuid.useCompression.OTHER").toBoolean() + + val useCompression = useCompressionWifi && useCompressionMobile && useCompressionOther + migrationsHelper.writeValue(db, "$accountUuid.useCompression", useCompression.toString()) + + migrationsHelper.writeValue(db, "$accountUuid.useCompression.WIFI", null) + migrationsHelper.writeValue(db, "$accountUuid.useCompression.MOBILE", null) + migrationsHelper.writeValue(db, "$accountUuid.useCompression.OTHER", null) + } +} diff --git a/app/storage/src/main/java/com/fsck/k9/preferences/migrations/StorageMigrations.kt b/app/storage/src/main/java/com/fsck/k9/preferences/migrations/StorageMigrations.kt index 8f435b07a6..07a6a091b8 100644 --- a/app/storage/src/main/java/com/fsck/k9/preferences/migrations/StorageMigrations.kt +++ b/app/storage/src/main/java/com/fsck/k9/preferences/migrations/StorageMigrations.kt @@ -23,5 +23,6 @@ internal object StorageMigrations { if (oldVersion < 15) StorageMigrationTo15(db, migrationsHelper).rewriteIdleRefreshInterval() if (oldVersion < 16) StorageMigrationTo16(db, migrationsHelper).changeDefaultRegisteredNameColor() if (oldVersion < 17) StorageMigrationTo17(db, migrationsHelper).rewriteNotificationLightSettings() + if (oldVersion < 18) StorageMigrationTo18(db, migrationsHelper).rewriteImapCompressionSettings() } } -- GitLab From 72e679dca05a5fd63b31b6b45dd561212d081c47 Mon Sep 17 00:00:00 2001 From: cketti Date: Thu, 7 Apr 2022 05:47:35 +0200 Subject: [PATCH 13/75] Add migration for IMAP compression in settings file --- .../AccountSettingsDescriptions.java | 31 +++++++++++++++++-- .../com/fsck/k9/preferences/Settings.java | 2 +- 2 files changed, 29 insertions(+), 4 deletions(-) diff --git a/app/core/src/main/java/com/fsck/k9/preferences/AccountSettingsDescriptions.java b/app/core/src/main/java/com/fsck/k9/preferences/AccountSettingsDescriptions.java index 9a295a335c..97fcd961d6 100644 --- a/app/core/src/main/java/com/fsck/k9/preferences/AccountSettingsDescriptions.java +++ b/app/core/src/main/java/com/fsck/k9/preferences/AccountSettingsDescriptions.java @@ -207,13 +207,16 @@ public class AccountSettingsDescriptions { new V(53, new StringSetting(null)) )); s.put("useCompression.MOBILE", Settings.versions( - new V(1, new BooleanSetting(true)) + new V(1, new BooleanSetting(true)), + new V(81, null) )); s.put("useCompression.OTHER", Settings.versions( - new V(1, new BooleanSetting(true)) + new V(1, new BooleanSetting(true)), + new V(81, null) )); s.put("useCompression.WIFI", Settings.versions( - new V(1, new BooleanSetting(true)) + new V(1, new BooleanSetting(true)), + new V(81, null) )); s.put("vibrate", Settings.versions( new V(1, new BooleanSetting(false)) @@ -270,6 +273,9 @@ public class AccountSettingsDescriptions { s.put("notificationLight", Settings.versions( new V(80, new EnumSetting<>(NotificationLight.class, NotificationLight.Disabled)) )); + s.put("useCompression", Settings.versions( + new V(81, new BooleanSetting(true)) + )); // note that there is no setting for openPgpProvider, because this will have to be set up together // with the actual provider after import anyways. @@ -280,6 +286,7 @@ public class AccountSettingsDescriptions { u.put(54, new SettingsUpgraderV54()); u.put(74, new SettingsUpgraderV74()); u.put(80, new SettingsUpgraderV80()); + u.put(81, new SettingsUpgraderV81()); UPGRADERS = Collections.unmodifiableMap(u); } @@ -546,4 +553,22 @@ public class AccountSettingsDescriptions { return SetsKt.setOf("led", "ledColor"); } } + + /** + * Rewrite the per-network type IMAP compression settings to a single setting. + */ + private static class SettingsUpgraderV81 implements SettingsUpgrader { + @Override + public Set upgrade(Map settings) { + Boolean useCompressionWifi = (Boolean) settings.get("useCompression.WIFI"); + Boolean useCompressionMobile = (Boolean) settings.get("useCompression.MOBILE"); + Boolean useCompressionOther = (Boolean) settings.get("useCompression.OTHER"); + + boolean useCompression = useCompressionWifi != null && useCompressionMobile != null && + useCompressionOther != null && useCompressionWifi && useCompressionMobile && useCompressionOther; + settings.put("useCompression", useCompression); + + return SetsKt.setOf("useCompression.WIFI", "useCompression.MOBILE", "useCompression.OTHER"); + } + } } diff --git a/app/core/src/main/java/com/fsck/k9/preferences/Settings.java b/app/core/src/main/java/com/fsck/k9/preferences/Settings.java index 8124a0c345..db1674f7c4 100644 --- a/app/core/src/main/java/com/fsck/k9/preferences/Settings.java +++ b/app/core/src/main/java/com/fsck/k9/preferences/Settings.java @@ -36,7 +36,7 @@ public class Settings { * * @see SettingsExporter */ - public static final int VERSION = 80; + public static final int VERSION = 81; static Map validate(int version, Map> settings, Map importedSettings, boolean useDefaultValues) { -- GitLab From c25884314fa75e1d479add438bbd663132786464 Mon Sep 17 00:00:00 2001 From: cketti Date: Thu, 14 Apr 2022 17:29:16 +0200 Subject: [PATCH 14/75] Rename .java to .kt --- .../smtp/{SmtpTransportTest.java => SmtpTransportTest.kt} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename mail/protocols/smtp/src/test/java/com/fsck/k9/mail/transport/smtp/{SmtpTransportTest.java => SmtpTransportTest.kt} (100%) diff --git a/mail/protocols/smtp/src/test/java/com/fsck/k9/mail/transport/smtp/SmtpTransportTest.java b/mail/protocols/smtp/src/test/java/com/fsck/k9/mail/transport/smtp/SmtpTransportTest.kt similarity index 100% rename from mail/protocols/smtp/src/test/java/com/fsck/k9/mail/transport/smtp/SmtpTransportTest.java rename to mail/protocols/smtp/src/test/java/com/fsck/k9/mail/transport/smtp/SmtpTransportTest.kt -- GitLab From cf2979e9fb6e44f9011a2a1e87bfcad21d9ed750 Mon Sep 17 00:00:00 2001 From: cketti Date: Thu, 14 Apr 2022 17:29:16 +0200 Subject: [PATCH 15/75] Convert `SmtpTransportTest` to Kotlin --- mail/protocols/smtp/build.gradle | 2 +- .../mail/transport/smtp/SmtpTransportTest.kt | 1566 +++++++++-------- 2 files changed, 794 insertions(+), 774 deletions(-) diff --git a/mail/protocols/smtp/build.gradle b/mail/protocols/smtp/build.gradle index 6707ad5f3c..478cd591b2 100644 --- a/mail/protocols/smtp/build.gradle +++ b/mail/protocols/smtp/build.gradle @@ -17,7 +17,7 @@ dependencies { testImplementation "org.robolectric:robolectric:${versions.robolectric}" testImplementation "junit:junit:${versions.junit}" testImplementation "com.google.truth:truth:${versions.truth}" - testImplementation "org.mockito:mockito-core:${versions.mockito}" + testImplementation "org.mockito.kotlin:mockito-kotlin:${versions.mockitoKotlin}" testImplementation "com.squareup.okio:okio:${versions.okio}" testImplementation "com.jcraft:jzlib:1.0.7" } diff --git a/mail/protocols/smtp/src/test/java/com/fsck/k9/mail/transport/smtp/SmtpTransportTest.kt b/mail/protocols/smtp/src/test/java/com/fsck/k9/mail/transport/smtp/SmtpTransportTest.kt index 49e197b1e9..a9ef9d8484 100644 --- a/mail/protocols/smtp/src/test/java/com/fsck/k9/mail/transport/smtp/SmtpTransportTest.kt +++ b/mail/protocols/smtp/src/test/java/com/fsck/k9/mail/transport/smtp/SmtpTransportTest.kt @@ -1,910 +1,930 @@ -package com.fsck.k9.mail.transport.smtp; +package com.fsck.k9.mail.transport.smtp + +import com.fsck.k9.mail.AuthType +import com.fsck.k9.mail.AuthenticationFailedException +import com.fsck.k9.mail.CertificateValidationException +import com.fsck.k9.mail.ConnectionSecurity +import com.fsck.k9.mail.K9LibRobolectricTestRunner +import com.fsck.k9.mail.Message +import com.fsck.k9.mail.MessagingException +import com.fsck.k9.mail.ServerSettings +import com.fsck.k9.mail.XOAuth2ChallengeParserTest +import com.fsck.k9.mail.filter.Base64 +import com.fsck.k9.mail.helpers.TestMessageBuilder +import com.fsck.k9.mail.helpers.TestTrustedSocketFactory +import com.fsck.k9.mail.internet.MimeMessage +import com.fsck.k9.mail.oauth.OAuth2TokenProvider +import com.fsck.k9.mail.transport.mockServer.MockSmtpServer +import com.google.common.truth.Truth.assertThat +import org.junit.Assert.fail +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentMatchers.anyLong +import org.mockito.ArgumentMatchers.anyString +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.doThrow +import org.mockito.kotlin.eq +import org.mockito.kotlin.inOrder +import org.mockito.kotlin.mock +import org.mockito.kotlin.stubbing + +private const val USERNAME = "user" +private const val PASSWORD = "password" +private val CLIENT_CERTIFICATE_ALIAS: String? = null + +@RunWith(K9LibRobolectricTestRunner::class) +class SmtpTransportTest { + private val socketFactory = TestTrustedSocketFactory.newInstance() + private val oAuth2TokenProvider = createMockOAuth2TokenProvider() + + @Test + fun `open() should provide hostname`() { + val server = MockSmtpServer().apply { + output("220 localhost Simple Mail Transfer Service Ready") + expect("EHLO [127.0.0.1]") + output("250-localhost Hello client.localhost") + output("250 OK") + } + val transport = startServerAndCreateSmtpTransport(server, password = null) + + transport.open() + + server.verifyConnectionStillOpen() + server.verifyInteractionCompleted() + } + + @Test + fun `open() without AUTH LOGIN extension should connect without authentication`() { + val server = MockSmtpServer().apply { + output("220 localhost Simple Mail Transfer Service Ready") + expect("EHLO [127.0.0.1]") + output("250-localhost Hello client.localhost") + output("250 OK") + } + val transport = startServerAndCreateSmtpTransportWithoutPassword(server) + + transport.open() + + server.verifyConnectionStillOpen() + server.verifyInteractionCompleted() + } + + @Test + fun `open() with AUTH PLAIN extension`() { + val server = MockSmtpServer().apply { + output("220 localhost Simple Mail Transfer Service Ready") + expect("EHLO [127.0.0.1]") + output("250-localhost Hello client.localhost") + output("250 AUTH PLAIN LOGIN") + expect("AUTH PLAIN AHVzZXIAcGFzc3dvcmQ=") + output("235 2.7.0 Authentication successful") + } + val transport = startServerAndCreateSmtpTransport(server, authenticationType = AuthType.PLAIN) + transport.open() -import com.fsck.k9.mail.AuthType; -import com.fsck.k9.mail.AuthenticationFailedException; -import com.fsck.k9.mail.CertificateValidationException; -import com.fsck.k9.mail.ConnectionSecurity; -import com.fsck.k9.mail.K9LibRobolectricTestRunner; -import com.fsck.k9.mail.Message; -import com.fsck.k9.mail.MessagingException; -import com.fsck.k9.mail.ServerSettings; -import com.fsck.k9.mail.XOAuth2ChallengeParserTest; -import com.fsck.k9.mail.filter.Base64; -import com.fsck.k9.mail.helpers.TestMessageBuilder; -import com.fsck.k9.mail.helpers.TestTrustedSocketFactory; -import com.fsck.k9.mail.internet.MimeMessage; -import com.fsck.k9.mail.oauth.OAuth2TokenProvider; -import com.fsck.k9.mail.ssl.TrustedSocketFactory; -import com.fsck.k9.mail.transport.mockServer.MockSmtpServer; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.InOrder; + server.verifyConnectionStillOpen() + server.verifyInteractionCompleted() + } -import static junit.framework.Assert.assertTrue; -import static junit.framework.Assert.fail; -import static org.junit.Assert.assertEquals; -import static org.mockito.ArgumentMatchers.anyLong; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.inOrder; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - - -@RunWith(K9LibRobolectricTestRunner.class) -public class SmtpTransportTest { - private static final String USERNAME = "user"; - private static final String PASSWORD = "password"; - private static final String CLIENT_CERTIFICATE_ALIAS = null; - - - private TrustedSocketFactory socketFactory; - private OAuth2TokenProvider oAuth2TokenProvider; - - - @Before - public void before() throws AuthenticationFailedException { - socketFactory = TestTrustedSocketFactory.newInstance(); - oAuth2TokenProvider = mock(OAuth2TokenProvider.class); - when(oAuth2TokenProvider.getToken(eq(USERNAME), anyLong())) - .thenReturn("oldToken").thenReturn("newToken"); - } - - @Test - public void open__shouldProvideHostname() throws Exception { - MockSmtpServer server = new MockSmtpServer(); - server.output("220 localhost Simple Mail Transfer Service Ready"); - server.expect("EHLO [127.0.0.1]"); - server.output("250-localhost Hello client.localhost"); - server.output("250 OK"); - SmtpTransport transport = startServerAndCreateSmtpTransport(server, AuthType.PLAIN, ConnectionSecurity.NONE, - null); - - transport.open(); - - server.verifyConnectionStillOpen(); - server.verifyInteractionCompleted(); - } - - @Test - public void open_withoutAuthLoginExtension_shouldConnectWithoutAuthentication() throws Exception { - MockSmtpServer server = new MockSmtpServer(); - server.output("220 localhost Simple Mail Transfer Service Ready"); - server.expect("EHLO [127.0.0.1]"); - server.output("250-localhost Hello client.localhost"); - server.output("250 OK"); - SmtpTransport transport = startServerAndCreateSmtpTransportWithoutPassword(server); - - transport.open(); - - server.verifyConnectionStillOpen(); - server.verifyInteractionCompleted(); - } - - @Test - public void open_withAuthPlainExtension() throws Exception { - MockSmtpServer server = new MockSmtpServer(); - server.output("220 localhost Simple Mail Transfer Service Ready"); - server.expect("EHLO [127.0.0.1]"); - server.output("250-localhost Hello client.localhost"); - server.output("250 AUTH PLAIN LOGIN"); - server.expect("AUTH PLAIN AHVzZXIAcGFzc3dvcmQ="); - server.output("235 2.7.0 Authentication successful"); - SmtpTransport transport = startServerAndCreateSmtpTransport(server, AuthType.PLAIN, ConnectionSecurity.NONE); - - transport.open(); - - server.verifyConnectionStillOpen(); - server.verifyInteractionCompleted(); - } - - @Test - public void open_withAuthLoginExtension() throws Exception { - MockSmtpServer server = new MockSmtpServer(); - server.output("220 localhost Simple Mail Transfer Service Ready"); - server.expect("EHLO [127.0.0.1]"); - server.output("250-localhost Hello client.localhost"); - server.output("250 AUTH LOGIN"); - server.expect("AUTH LOGIN"); - server.output("250 OK"); - server.expect("dXNlcg=="); - server.output("250 OK"); - server.expect("cGFzc3dvcmQ="); - server.output("235 2.7.0 Authentication successful"); - SmtpTransport transport = startServerAndCreateSmtpTransport(server, AuthType.PLAIN, ConnectionSecurity.NONE); + @Test + fun `open() with AUTH LOGIN extension`() { + val server = MockSmtpServer().apply { + output("220 localhost Simple Mail Transfer Service Ready") + expect("EHLO [127.0.0.1]") + output("250-localhost Hello client.localhost") + output("250 AUTH LOGIN") + expect("AUTH LOGIN") + output("250 OK") + expect("dXNlcg==") + output("250 OK") + expect("cGFzc3dvcmQ=") + output("235 2.7.0 Authentication successful") + } + val transport = startServerAndCreateSmtpTransport(server, authenticationType = AuthType.PLAIN) - transport.open(); + transport.open() - server.verifyConnectionStillOpen(); - server.verifyInteractionCompleted(); + server.verifyConnectionStillOpen() + server.verifyInteractionCompleted() } @Test - public void open_withoutLoginAndPlainAuthExtensions_shouldThrow() throws Exception { - MockSmtpServer server = new MockSmtpServer(); - server.output("220 localhost Simple Mail Transfer Service Ready"); - server.expect("EHLO [127.0.0.1]"); - server.output("250-localhost Hello client.localhost"); - server.output("250 AUTH"); - server.expect("QUIT"); - server.output("221 BYE"); - SmtpTransport transport = startServerAndCreateSmtpTransport(server, AuthType.PLAIN, ConnectionSecurity.NONE); + fun `open() without LOGIN and PLAIN AUTH extensions should throw`() { + val server = MockSmtpServer().apply { + output("220 localhost Simple Mail Transfer Service Ready") + expect("EHLO [127.0.0.1]") + output("250-localhost Hello client.localhost") + output("250 AUTH") + expect("QUIT") + output("221 BYE") + } + val transport = startServerAndCreateSmtpTransport(server, authenticationType = AuthType.PLAIN) try { - transport.open(); - fail("Exception expected"); - } catch (MessagingException e) { - assertEquals("Authentication methods SASL PLAIN and LOGIN are unavailable.", e.getMessage()); + transport.open() + fail("Exception expected") + } catch (e: MessagingException) { + assertThat(e).hasMessageThat().isEqualTo("Authentication methods SASL PLAIN and LOGIN are unavailable.") } - server.verifyConnectionClosed(); - server.verifyInteractionCompleted(); + server.verifyConnectionClosed() + server.verifyInteractionCompleted() } @Test - public void open_withCramMd5AuthExtension() throws Exception { - MockSmtpServer server = new MockSmtpServer(); - server.output("220 localhost Simple Mail Transfer Service Ready"); - server.expect("EHLO [127.0.0.1]"); - server.output("250-localhost Hello client.localhost"); - server.output("250 AUTH CRAM-MD5"); - server.expect("AUTH CRAM-MD5"); - server.output("334 " + Base64.encode("<24609.1047914046@localhost>")); - server.expect("dXNlciAyZDBlNTcwYzZlYWI0ZjY3ZDUyZmFkN2Q1NGExZDJhYQ=="); - server.output("235 2.7.0 Authentication successful"); - SmtpTransport transport = startServerAndCreateSmtpTransport(server, AuthType.CRAM_MD5, ConnectionSecurity.NONE); + fun `open() with CRAM-MD5 AUTH extension`() { + val server = MockSmtpServer().apply { + output("220 localhost Simple Mail Transfer Service Ready") + expect("EHLO [127.0.0.1]") + output("250-localhost Hello client.localhost") + output("250 AUTH CRAM-MD5") + expect("AUTH CRAM-MD5") + output("334 " + Base64.encode("<24609.1047914046@localhost>")) + expect("dXNlciAyZDBlNTcwYzZlYWI0ZjY3ZDUyZmFkN2Q1NGExZDJhYQ==") + output("235 2.7.0 Authentication successful") + } + val transport = startServerAndCreateSmtpTransport(server, authenticationType = AuthType.CRAM_MD5) - transport.open(); + transport.open() - server.verifyConnectionStillOpen(); - server.verifyInteractionCompleted(); + server.verifyConnectionStillOpen() + server.verifyInteractionCompleted() } @Test - public void open_withoutCramMd5AuthExtension_shouldThrow() throws Exception { - MockSmtpServer server = new MockSmtpServer(); - server.output("220 localhost Simple Mail Transfer Service Ready"); - server.expect("EHLO [127.0.0.1]"); - server.output("250-localhost Hello client.localhost"); - server.output("250 AUTH PLAIN LOGIN"); - server.expect("QUIT"); - server.output("221 BYE"); - SmtpTransport transport = startServerAndCreateSmtpTransport(server, AuthType.CRAM_MD5, ConnectionSecurity.NONE); + fun `open() without CRAM-MD5 AUTH extension should throw`() { + val server = MockSmtpServer().apply { + output("220 localhost Simple Mail Transfer Service Ready") + expect("EHLO [127.0.0.1]") + output("250-localhost Hello client.localhost") + output("250 AUTH PLAIN LOGIN") + expect("QUIT") + output("221 BYE") + } + val transport = startServerAndCreateSmtpTransport(server, authenticationType = AuthType.CRAM_MD5) try { - transport.open(); - fail("Exception expected"); - } catch (MessagingException e) { - assertEquals("Authentication method CRAM-MD5 is unavailable.", e.getMessage()); + transport.open() + fail("Exception expected") + } catch (e: MessagingException) { + assertThat(e).hasMessageThat().isEqualTo("Authentication method CRAM-MD5 is unavailable.") } - server.verifyConnectionClosed(); - server.verifyInteractionCompleted(); + server.verifyConnectionClosed() + server.verifyInteractionCompleted() } @Test - public void open_withXoauth2Extension() throws Exception { - MockSmtpServer server = new MockSmtpServer(); - server.output("220 localhost Simple Mail Transfer Service Ready"); - server.expect("EHLO [127.0.0.1]"); - server.output("250-localhost Hello client.localhost"); - server.output("250 AUTH XOAUTH2"); - server.expect("AUTH XOAUTH2 dXNlcj11c2VyAWF1dGg9QmVhcmVyIG9sZFRva2VuAQE="); - server.output("235 2.7.0 Authentication successful"); - SmtpTransport transport = startServerAndCreateSmtpTransport(server, AuthType.XOAUTH2, ConnectionSecurity.NONE); + fun `open() with XOAUTH2 extension`() { + val server = MockSmtpServer().apply { + output("220 localhost Simple Mail Transfer Service Ready") + expect("EHLO [127.0.0.1]") + output("250-localhost Hello client.localhost") + output("250 AUTH XOAUTH2") + expect("AUTH XOAUTH2 dXNlcj11c2VyAWF1dGg9QmVhcmVyIG9sZFRva2VuAQE=") + output("235 2.7.0 Authentication successful") + } + val transport = startServerAndCreateSmtpTransport(server, authenticationType = AuthType.XOAUTH2) - transport.open(); + transport.open() - server.verifyConnectionStillOpen(); - server.verifyInteractionCompleted(); + server.verifyConnectionStillOpen() + server.verifyInteractionCompleted() } @Test - public void open_withXoauth2Extension_shouldThrowOn401Response() throws Exception { - MockSmtpServer server = new MockSmtpServer(); - server.output("220 localhost Simple Mail Transfer Service Ready"); - server.expect("EHLO [127.0.0.1]"); - server.output("250-localhost Hello client.localhost"); - server.output("250 AUTH XOAUTH2"); - server.expect("AUTH XOAUTH2 dXNlcj11c2VyAWF1dGg9QmVhcmVyIG9sZFRva2VuAQE="); - server.output("334 "+ XOAuth2ChallengeParserTest.STATUS_401_RESPONSE); - server.expect(""); - server.output("535-5.7.1 Username and Password not accepted. Learn more at"); - server.output("535 5.7.1 http://support.google.com/mail/bin/answer.py?answer=14257 hx9sm5317360pbc.68"); - server.expect("QUIT"); - server.output("221 BYE"); - SmtpTransport transport = startServerAndCreateSmtpTransport(server, AuthType.XOAUTH2, ConnectionSecurity.NONE); + fun `open() with XOAUTH2 extension should throw on 401 response`() { + val server = MockSmtpServer().apply { + output("220 localhost Simple Mail Transfer Service Ready") + expect("EHLO [127.0.0.1]") + output("250-localhost Hello client.localhost") + output("250 AUTH XOAUTH2") + expect("AUTH XOAUTH2 dXNlcj11c2VyAWF1dGg9QmVhcmVyIG9sZFRva2VuAQE=") + output("334 " + XOAuth2ChallengeParserTest.STATUS_401_RESPONSE) + expect("") + output("535-5.7.1 Username and Password not accepted. Learn more at") + output("535 5.7.1 http://support.google.com/mail/bin/answer.py?answer=14257 hx9sm5317360pbc.68") + expect("QUIT") + output("221 BYE") + } + val transport = startServerAndCreateSmtpTransport(server, authenticationType = AuthType.XOAUTH2) try { - transport.open(); - fail("Exception expected"); - } catch (AuthenticationFailedException e) { - assertEquals( - "5.7.1 Username and Password not accepted. Learn more at " + - "5.7.1 http://support.google.com/mail/bin/answer.py?answer=14257 hx9sm5317360pbc.68", - e.getMessage()); - } - - InOrder inOrder = inOrder(oAuth2TokenProvider); - inOrder.verify(oAuth2TokenProvider).getToken(eq(USERNAME), anyLong()); - inOrder.verify(oAuth2TokenProvider).invalidateToken(USERNAME); - server.verifyConnectionClosed(); - server.verifyInteractionCompleted(); - } - - @Test - public void open_withXoauth2Extension_shouldInvalidateAndRetryOn400Response() throws Exception { - MockSmtpServer server = new MockSmtpServer(); - server.output("220 localhost Simple Mail Transfer Service Ready"); - server.expect("EHLO [127.0.0.1]"); - server.output("250-localhost Hello client.localhost"); - server.output("250 AUTH XOAUTH2"); - server.expect("AUTH XOAUTH2 dXNlcj11c2VyAWF1dGg9QmVhcmVyIG9sZFRva2VuAQE="); - server.output("334 "+ XOAuth2ChallengeParserTest.STATUS_400_RESPONSE); - server.expect(""); - server.output("535-5.7.1 Username and Password not accepted. Learn more at"); - server.output("535 5.7.1 http://support.google.com/mail/bin/answer.py?answer=14257 hx9sm5317360pbc.68"); - server.expect("AUTH XOAUTH2 dXNlcj11c2VyAWF1dGg9QmVhcmVyIG5ld1Rva2VuAQE="); - server.output("235 2.7.0 Authentication successful"); - SmtpTransport transport = startServerAndCreateSmtpTransport(server, AuthType.XOAUTH2, ConnectionSecurity.NONE); - - transport.open(); - - InOrder inOrder = inOrder(oAuth2TokenProvider); - inOrder.verify(oAuth2TokenProvider).getToken(eq(USERNAME), anyLong()); - inOrder.verify(oAuth2TokenProvider).invalidateToken(USERNAME); - inOrder.verify(oAuth2TokenProvider).getToken(eq(USERNAME), anyLong()); - server.verifyConnectionStillOpen(); - server.verifyInteractionCompleted(); - } - - @Test - public void open_withXoauth2Extension_shouldInvalidateAndRetryOnInvalidJsonResponse() throws Exception { - MockSmtpServer server = new MockSmtpServer(); - server.output("220 localhost Simple Mail Transfer Service Ready"); - server.expect("EHLO [127.0.0.1]"); - server.output("250-localhost Hello client.localhost"); - server.output("250 AUTH XOAUTH2"); - server.expect("AUTH XOAUTH2 dXNlcj11c2VyAWF1dGg9QmVhcmVyIG9sZFRva2VuAQE="); - server.output("334 "+ XOAuth2ChallengeParserTest.INVALID_RESPONSE); - server.expect(""); - server.output("535-5.7.1 Username and Password not accepted. Learn more at"); - server.output("535 5.7.1 http://support.google.com/mail/bin/answer.py?answer=14257 hx9sm5317360pbc.68"); - server.expect("AUTH XOAUTH2 dXNlcj11c2VyAWF1dGg9QmVhcmVyIG5ld1Rva2VuAQE="); - server.output("235 2.7.0 Authentication successful"); - SmtpTransport transport = startServerAndCreateSmtpTransport(server, AuthType.XOAUTH2, ConnectionSecurity.NONE); - - transport.open(); - - InOrder inOrder = inOrder(oAuth2TokenProvider); - inOrder.verify(oAuth2TokenProvider).getToken(eq(USERNAME), anyLong()); - inOrder.verify(oAuth2TokenProvider).invalidateToken(USERNAME); - inOrder.verify(oAuth2TokenProvider).getToken(eq(USERNAME), anyLong()); - server.verifyConnectionStillOpen(); - server.verifyInteractionCompleted(); - } - - @Test - public void open_withXoauth2Extension_shouldInvalidateAndRetryOnMissingStatusJsonResponse() throws Exception { - MockSmtpServer server = new MockSmtpServer(); - server.output("220 localhost Simple Mail Transfer Service Ready"); - server.expect("EHLO [127.0.0.1]"); - server.output("250-localhost Hello client.localhost"); - server.output("250 AUTH XOAUTH2"); - server.expect("AUTH XOAUTH2 dXNlcj11c2VyAWF1dGg9QmVhcmVyIG9sZFRva2VuAQE="); - server.output("334 "+ XOAuth2ChallengeParserTest.MISSING_STATUS_RESPONSE); - server.expect(""); - server.output("535-5.7.1 Username and Password not accepted. Learn more at"); - server.output("535 5.7.1 http://support.google.com/mail/bin/answer.py?answer=14257 hx9sm5317360pbc.68"); - server.expect("AUTH XOAUTH2 dXNlcj11c2VyAWF1dGg9QmVhcmVyIG5ld1Rva2VuAQE="); - server.output("235 2.7.0 Authentication successful"); - SmtpTransport transport = startServerAndCreateSmtpTransport(server, AuthType.XOAUTH2, ConnectionSecurity.NONE); - - transport.open(); - - InOrder inOrder = inOrder(oAuth2TokenProvider); - inOrder.verify(oAuth2TokenProvider).getToken(eq(USERNAME), anyLong()); - inOrder.verify(oAuth2TokenProvider).invalidateToken(USERNAME); - inOrder.verify(oAuth2TokenProvider).getToken(eq(USERNAME), anyLong()); - server.verifyConnectionStillOpen(); - server.verifyInteractionCompleted(); - } - - @Test - public void open_withXoauth2Extension_shouldThrowOnMultipleFailure() throws Exception { - MockSmtpServer server = new MockSmtpServer(); - server.output("220 localhost Simple Mail Transfer Service Ready"); - server.expect("EHLO [127.0.0.1]"); - server.output("250-localhost Hello client.localhost"); - server.output("250 AUTH XOAUTH2"); - server.expect("AUTH XOAUTH2 dXNlcj11c2VyAWF1dGg9QmVhcmVyIG9sZFRva2VuAQE="); - server.output("334 " + XOAuth2ChallengeParserTest.STATUS_400_RESPONSE); - server.expect(""); - server.output("535-5.7.1 Username and Password not accepted. Learn more at"); - server.output("535 5.7.1 http://support.google.com/mail/bin/answer.py?answer=14257 hx9sm5317360pbc.68"); - server.expect("AUTH XOAUTH2 dXNlcj11c2VyAWF1dGg9QmVhcmVyIG5ld1Rva2VuAQE="); - server.output("334 " + XOAuth2ChallengeParserTest.STATUS_400_RESPONSE); - server.expect(""); - server.output("535-5.7.1 Username and Password not accepted. Learn more at"); - server.output("535 5.7.1 http://support.google.com/mail/bin/answer.py?answer=14257 hx9sm5317360pbc.68"); - server.expect("QUIT"); - server.output("221 BYE"); - - SmtpTransport transport = startServerAndCreateSmtpTransport(server, AuthType.XOAUTH2, ConnectionSecurity.NONE); + transport.open() + fail("Exception expected") + } catch (e: AuthenticationFailedException) { + assertThat(e).hasMessageThat().isEqualTo( + "5.7.1 Username and Password not accepted. Learn more at " + + "5.7.1 http://support.google.com/mail/bin/answer.py?answer=14257 hx9sm5317360pbc.68" + ) + } + + inOrder(oAuth2TokenProvider) { + verify(oAuth2TokenProvider).getToken(eq(USERNAME), anyLong()) + verify(oAuth2TokenProvider).invalidateToken(USERNAME) + } + server.verifyConnectionClosed() + server.verifyInteractionCompleted() + } + + @Test + fun `open() with XOAUTH2 extension should invalidate and retry on 400 response`() { + val server = MockSmtpServer().apply { + output("220 localhost Simple Mail Transfer Service Ready") + expect("EHLO [127.0.0.1]") + output("250-localhost Hello client.localhost") + output("250 AUTH XOAUTH2") + expect("AUTH XOAUTH2 dXNlcj11c2VyAWF1dGg9QmVhcmVyIG9sZFRva2VuAQE=") + output("334 " + XOAuth2ChallengeParserTest.STATUS_400_RESPONSE) + expect("") + output("535-5.7.1 Username and Password not accepted. Learn more at") + output("535 5.7.1 http://support.google.com/mail/bin/answer.py?answer=14257 hx9sm5317360pbc.68") + expect("AUTH XOAUTH2 dXNlcj11c2VyAWF1dGg9QmVhcmVyIG5ld1Rva2VuAQE=") + output("235 2.7.0 Authentication successful") + } + val transport = startServerAndCreateSmtpTransport(server, authenticationType = AuthType.XOAUTH2) + + transport.open() + + inOrder(oAuth2TokenProvider) { + verify(oAuth2TokenProvider).getToken(eq(USERNAME), anyLong()) + verify(oAuth2TokenProvider).invalidateToken(USERNAME) + verify(oAuth2TokenProvider).getToken(eq(USERNAME), anyLong()) + } + server.verifyConnectionStillOpen() + server.verifyInteractionCompleted() + } + + @Test + fun `open() with XOAUTH2 extension should invalidate and retry on invalid JSON response`() { + val server = MockSmtpServer().apply { + output("220 localhost Simple Mail Transfer Service Ready") + expect("EHLO [127.0.0.1]") + output("250-localhost Hello client.localhost") + output("250 AUTH XOAUTH2") + expect("AUTH XOAUTH2 dXNlcj11c2VyAWF1dGg9QmVhcmVyIG9sZFRva2VuAQE=") + output("334 " + XOAuth2ChallengeParserTest.INVALID_RESPONSE) + expect("") + output("535-5.7.1 Username and Password not accepted. Learn more at") + output("535 5.7.1 http://support.google.com/mail/bin/answer.py?answer=14257 hx9sm5317360pbc.68") + expect("AUTH XOAUTH2 dXNlcj11c2VyAWF1dGg9QmVhcmVyIG5ld1Rva2VuAQE=") + output("235 2.7.0 Authentication successful") + } + val transport = startServerAndCreateSmtpTransport(server, authenticationType = AuthType.XOAUTH2) + + transport.open() + + inOrder(oAuth2TokenProvider) { + verify(oAuth2TokenProvider).getToken(eq(USERNAME), anyLong()) + verify(oAuth2TokenProvider).invalidateToken(USERNAME) + verify(oAuth2TokenProvider).getToken(eq(USERNAME), anyLong()) + } + server.verifyConnectionStillOpen() + server.verifyInteractionCompleted() + } + + @Test + fun `open() with XOAUTH2 extension should invalidate and retry on missing status JSON response`() { + val server = MockSmtpServer().apply { + output("220 localhost Simple Mail Transfer Service Ready") + expect("EHLO [127.0.0.1]") + output("250-localhost Hello client.localhost") + output("250 AUTH XOAUTH2") + expect("AUTH XOAUTH2 dXNlcj11c2VyAWF1dGg9QmVhcmVyIG9sZFRva2VuAQE=") + output("334 " + XOAuth2ChallengeParserTest.MISSING_STATUS_RESPONSE) + expect("") + output("535-5.7.1 Username and Password not accepted. Learn more at") + output("535 5.7.1 http://support.google.com/mail/bin/answer.py?answer=14257 hx9sm5317360pbc.68") + expect("AUTH XOAUTH2 dXNlcj11c2VyAWF1dGg9QmVhcmVyIG5ld1Rva2VuAQE=") + output("235 2.7.0 Authentication successful") + } + val transport = startServerAndCreateSmtpTransport(server, authenticationType = AuthType.XOAUTH2) + + transport.open() + + inOrder(oAuth2TokenProvider) { + verify(oAuth2TokenProvider).getToken(eq(USERNAME), anyLong()) + verify(oAuth2TokenProvider).invalidateToken(USERNAME) + verify(oAuth2TokenProvider).getToken(eq(USERNAME), anyLong()) + } + server.verifyConnectionStillOpen() + server.verifyInteractionCompleted() + } + + @Test + fun `open() with XOAUTH2 extension should throw on multiple failures`() { + val server = MockSmtpServer().apply { + output("220 localhost Simple Mail Transfer Service Ready") + expect("EHLO [127.0.0.1]") + output("250-localhost Hello client.localhost") + output("250 AUTH XOAUTH2") + expect("AUTH XOAUTH2 dXNlcj11c2VyAWF1dGg9QmVhcmVyIG9sZFRva2VuAQE=") + output("334 " + XOAuth2ChallengeParserTest.STATUS_400_RESPONSE) + expect("") + output("535-5.7.1 Username and Password not accepted. Learn more at") + output("535 5.7.1 http://support.google.com/mail/bin/answer.py?answer=14257 hx9sm5317360pbc.68") + expect("AUTH XOAUTH2 dXNlcj11c2VyAWF1dGg9QmVhcmVyIG5ld1Rva2VuAQE=") + output("334 " + XOAuth2ChallengeParserTest.STATUS_400_RESPONSE) + expect("") + output("535-5.7.1 Username and Password not accepted. Learn more at") + output("535 5.7.1 http://support.google.com/mail/bin/answer.py?answer=14257 hx9sm5317360pbc.68") + expect("QUIT") + output("221 BYE") + } + + val transport = startServerAndCreateSmtpTransport(server, authenticationType = AuthType.XOAUTH2) try { - transport.open(); - fail("Exception expected"); - } catch (AuthenticationFailedException e) { - assertEquals( + transport.open() + fail("Exception expected") + } catch (e: AuthenticationFailedException) { + assertThat(e).hasMessageThat().isEqualTo( "5.7.1 Username and Password not accepted. Learn more at " + - "5.7.1 http://support.google.com/mail/bin/answer.py?answer=14257 hx9sm5317360pbc.68", - e.getMessage()); + "5.7.1 http://support.google.com/mail/bin/answer.py?answer=14257 hx9sm5317360pbc.68" + ) } - server.verifyConnectionClosed(); - server.verifyInteractionCompleted(); + server.verifyConnectionClosed() + server.verifyInteractionCompleted() } @Test - public void open_withXoauth2Extension_shouldThrowOnFailure_fetchingToken() throws Exception { - MockSmtpServer server = new MockSmtpServer(); - server.output("220 localhost Simple Mail Transfer Service Ready"); - server.expect("EHLO [127.0.0.1]"); - server.output("250-localhost Hello client.localhost"); - server.output("250 AUTH XOAUTH2"); - server.expect("QUIT"); - server.output("221 BYE"); - when(oAuth2TokenProvider.getToken(anyString(), anyLong())) - .thenThrow(new AuthenticationFailedException("Failed to fetch token")); - SmtpTransport transport = startServerAndCreateSmtpTransport(server, AuthType.XOAUTH2, ConnectionSecurity.NONE); + fun `open() with XOAUTH2 extension should throw on failure to fetch token`() { + val server = MockSmtpServer().apply { + output("220 localhost Simple Mail Transfer Service Ready") + expect("EHLO [127.0.0.1]") + output("250-localhost Hello client.localhost") + output("250 AUTH XOAUTH2") + expect("QUIT") + output("221 BYE") + } + stubbing(oAuth2TokenProvider) { + on { getToken(anyString(), anyLong()) } doThrow AuthenticationFailedException("Failed to fetch token") + } + val transport = startServerAndCreateSmtpTransport(server, authenticationType = AuthType.XOAUTH2) try { - transport.open(); - fail("Exception expected"); - } catch (AuthenticationFailedException e) { - assertEquals("Failed to fetch token", e.getMessage()); + transport.open() + fail("Exception expected") + } catch (e: AuthenticationFailedException) { + assertThat(e).hasMessageThat().isEqualTo("Failed to fetch token") } - server.verifyConnectionClosed(); - server.verifyInteractionCompleted(); + server.verifyConnectionClosed() + server.verifyInteractionCompleted() } @Test - public void open_withoutXoauth2Extension_shouldThrow() throws Exception { - MockSmtpServer server = new MockSmtpServer(); - server.output("220 localhost Simple Mail Transfer Service Ready"); - server.expect("EHLO [127.0.0.1]"); - server.output("250-localhost Hello client.localhost"); - server.output("250 AUTH PLAIN LOGIN"); - server.expect("QUIT"); - server.output("221 BYE"); - SmtpTransport transport = startServerAndCreateSmtpTransport(server, AuthType.XOAUTH2, ConnectionSecurity.NONE); + fun `open() without XOAUTH2 extension should throw`() { + val server = MockSmtpServer().apply { + output("220 localhost Simple Mail Transfer Service Ready") + expect("EHLO [127.0.0.1]") + output("250-localhost Hello client.localhost") + output("250 AUTH PLAIN LOGIN") + expect("QUIT") + output("221 BYE") + } + val transport = startServerAndCreateSmtpTransport(server, authenticationType = AuthType.XOAUTH2) try { - transport.open(); - fail("Exception expected"); - } catch (MessagingException e) { - assertEquals("Authentication method XOAUTH2 is unavailable.", e.getMessage()); + transport.open() + fail("Exception expected") + } catch (e: MessagingException) { + assertThat(e).hasMessageThat().isEqualTo("Authentication method XOAUTH2 is unavailable.") } - server.verifyConnectionClosed(); - server.verifyInteractionCompleted(); + server.verifyConnectionClosed() + server.verifyInteractionCompleted() } @Test - public void open_withAuthExternalExtension() throws Exception { - MockSmtpServer server = new MockSmtpServer(); - server.output("220 localhost Simple Mail Transfer Service Ready"); - server.expect("EHLO [127.0.0.1]"); - server.output("250-localhost Hello client.localhost"); - server.output("250 AUTH EXTERNAL"); - server.expect("AUTH EXTERNAL dXNlcg=="); - server.output("235 2.7.0 Authentication successful"); - SmtpTransport transport = startServerAndCreateSmtpTransport(server, AuthType.EXTERNAL, ConnectionSecurity.NONE); + fun `open() with AUTH EXTERNAL extension`() { + val server = MockSmtpServer().apply { + output("220 localhost Simple Mail Transfer Service Ready") + expect("EHLO [127.0.0.1]") + output("250-localhost Hello client.localhost") + output("250 AUTH EXTERNAL") + expect("AUTH EXTERNAL dXNlcg==") + output("235 2.7.0 Authentication successful") + } + val transport = startServerAndCreateSmtpTransport(server, authenticationType = AuthType.EXTERNAL) - transport.open(); + transport.open() - server.verifyConnectionStillOpen(); - server.verifyInteractionCompleted(); + server.verifyConnectionStillOpen() + server.verifyInteractionCompleted() } @Test - public void open_withoutAuthExternalExtension_shouldThrow() throws Exception { - MockSmtpServer server = new MockSmtpServer(); - server.output("220 localhost Simple Mail Transfer Service Ready"); - server.expect("EHLO [127.0.0.1]"); - server.output("250-localhost Hello client.localhost"); - server.output("250 AUTH"); - server.expect("QUIT"); - server.output("221 BYE"); - SmtpTransport transport = startServerAndCreateSmtpTransport(server, AuthType.EXTERNAL, ConnectionSecurity.NONE); + fun `open() without AUTH EXTERNAL extension should throw`() { + val server = MockSmtpServer().apply { + output("220 localhost Simple Mail Transfer Service Ready") + expect("EHLO [127.0.0.1]") + output("250-localhost Hello client.localhost") + output("250 AUTH") + expect("QUIT") + output("221 BYE") + } + val transport = startServerAndCreateSmtpTransport(server, authenticationType = AuthType.EXTERNAL) try { - transport.open(); - fail("Exception expected"); - } catch (CertificateValidationException e) { - assertEquals(CertificateValidationException.Reason.MissingCapability, e.getReason()); + transport.open() + fail("Exception expected") + } catch (e: CertificateValidationException) { + assertThat(e.reason).isEqualTo(CertificateValidationException.Reason.MissingCapability) } - server.verifyConnectionClosed(); - server.verifyInteractionCompleted(); + server.verifyConnectionClosed() + server.verifyInteractionCompleted() } @Test - public void open_withAutomaticAuthAndNoTransportSecurityAndAuthCramMd5Extension_shouldUseAuthCramMd5() - throws Exception { - MockSmtpServer server = new MockSmtpServer(); - server.output("220 localhost Simple Mail Transfer Service Ready"); - server.expect("EHLO [127.0.0.1]"); - server.output("250-localhost Hello client.localhost"); - server.output("250 AUTH CRAM-MD5"); - server.expect("AUTH CRAM-MD5"); - server.output("334 " + Base64.encode("<24609.1047914046@localhost>")); - server.expect("dXNlciAyZDBlNTcwYzZlYWI0ZjY3ZDUyZmFkN2Q1NGExZDJhYQ=="); - server.output("235 2.7.0 Authentication successful"); - SmtpTransport transport = startServerAndCreateSmtpTransport(server, AuthType.AUTOMATIC, - ConnectionSecurity.NONE); + fun `open() with automatic auth and no transport security and AUTH CRAM-MD5 extension should use CRAM-MD5`() { + val server = MockSmtpServer().apply { + output("220 localhost Simple Mail Transfer Service Ready") + expect("EHLO [127.0.0.1]") + output("250-localhost Hello client.localhost") + output("250 AUTH CRAM-MD5") + expect("AUTH CRAM-MD5") + output("334 " + Base64.encode("<24609.1047914046@localhost>")) + expect("dXNlciAyZDBlNTcwYzZlYWI0ZjY3ZDUyZmFkN2Q1NGExZDJhYQ==") + output("235 2.7.0 Authentication successful") + } + val transport = startServerAndCreateSmtpTransport( + server, + authenticationType = AuthType.AUTOMATIC, + connectionSecurity = ConnectionSecurity.NONE + ) - transport.open(); + transport.open() - server.verifyConnectionStillOpen(); - server.verifyInteractionCompleted(); + server.verifyConnectionStillOpen() + server.verifyInteractionCompleted() } @Test - public void open_withAutomaticAuthAndNoTransportSecurityAndAuthPlainExtension_shouldThrow() throws Exception { - MockSmtpServer server = new MockSmtpServer(); - server.output("220 localhost Simple Mail Transfer Service Ready"); - server.expect("EHLO [127.0.0.1]"); - server.output("250-localhost Hello client.localhost"); - server.output("250 AUTH PLAIN LOGIN"); - server.expect("QUIT"); - server.output("221 BYE"); - SmtpTransport transport = startServerAndCreateSmtpTransport(server, AuthType.AUTOMATIC, - ConnectionSecurity.NONE); + fun `open() with automatic auth and no transport security and AUTH PLAIN extension should throw`() { + val server = MockSmtpServer() + server.output("220 localhost Simple Mail Transfer Service Ready") + server.expect("EHLO [127.0.0.1]") + server.output("250-localhost Hello client.localhost") + server.output("250 AUTH PLAIN LOGIN") + server.expect("QUIT") + server.output("221 BYE") + val transport = startServerAndCreateSmtpTransport( + server, + authenticationType = AuthType.AUTOMATIC, + connectionSecurity = ConnectionSecurity.NONE + ) try { - transport.open(); - fail("Exception expected"); - } catch (MessagingException e) { - assertEquals("Update your outgoing server authentication setting. AUTOMATIC auth. is unavailable.", - e.getMessage()); + transport.open() + fail("Exception expected") + } catch (e: MessagingException) { + assertThat(e).hasMessageThat().isEqualTo( + "Update your outgoing server authentication setting. AUTOMATIC auth. is unavailable." + ) } - server.verifyConnectionClosed(); - server.verifyInteractionCompleted(); + server.verifyConnectionClosed() + server.verifyInteractionCompleted() } @Test - public void open_withEhloFailing_shouldTryHelo() throws Exception { - MockSmtpServer server = new MockSmtpServer(); - server.output("220 localhost Simple Mail Transfer Service Ready"); - server.expect("EHLO [127.0.0.1]"); - server.output("502 5.5.1, Unrecognized command."); - server.expect("HELO [127.0.0.1]"); - server.output("250 localhost"); - SmtpTransport transport = startServerAndCreateSmtpTransportWithoutPassword(server); + fun `open() with EHLO failing should try HELO`() { + val server = MockSmtpServer().apply { + output("220 localhost Simple Mail Transfer Service Ready") + expect("EHLO [127.0.0.1]") + output("502 5.5.1, Unrecognized command.") + expect("HELO [127.0.0.1]") + output("250 localhost") + } + val transport = startServerAndCreateSmtpTransportWithoutPassword(server) - transport.open(); + transport.open() - server.verifyConnectionStillOpen(); - server.verifyInteractionCompleted(); + server.verifyConnectionStillOpen() + server.verifyInteractionCompleted() } @Test - public void open_withSupportWithEnhancedStatusCodesOnAuthFailure_shouldThrowEncodedMessage() - throws Exception { - MockSmtpServer server = new MockSmtpServer(); - server.output("220 localhost Simple Mail Transfer Service Ready"); - server.expect("EHLO [127.0.0.1]"); - server.output("250-localhost Hello client.localhost"); - server.output("250-ENHANCEDSTATUSCODES"); - server.output("250 AUTH XOAUTH2"); - server.expect("AUTH XOAUTH2 dXNlcj11c2VyAWF1dGg9QmVhcmVyIG9sZFRva2VuAQE="); - server.output("334 " + XOAuth2ChallengeParserTest.STATUS_401_RESPONSE); - server.expect(""); - server.output("535-5.7.1 Username and Password not accepted. Learn more at"); - server.output("535 5.7.1 http://support.google.com/mail/bin/answer.py?answer=14257 hx9sm5317360pbc.68"); - server.expect("QUIT"); - server.output("221 BYE"); - SmtpTransport transport = startServerAndCreateSmtpTransport(server, AuthType.XOAUTH2, ConnectionSecurity.NONE); + fun `open() with support for ENHANCEDSTATUSCODES should throw strip enhanced status codes from error message`() { + val server = MockSmtpServer() + server.output("220 localhost Simple Mail Transfer Service Ready") + server.expect("EHLO [127.0.0.1]") + server.output("250-localhost Hello client.localhost") + server.output("250-ENHANCEDSTATUSCODES") + server.output("250 AUTH XOAUTH2") + server.expect("AUTH XOAUTH2 dXNlcj11c2VyAWF1dGg9QmVhcmVyIG9sZFRva2VuAQE=") + server.output("334 " + XOAuth2ChallengeParserTest.STATUS_401_RESPONSE) + server.expect("") + server.output("535-5.7.1 Username and Password not accepted. Learn more at") + server.output("535 5.7.1 http://support.google.com/mail/bin/answer.py?answer=14257 hx9sm5317360pbc.68") + server.expect("QUIT") + server.output("221 BYE") + val transport = startServerAndCreateSmtpTransport(server, authenticationType = AuthType.XOAUTH2) try { - transport.open(); - fail("Exception expected"); - } catch (AuthenticationFailedException e) { - assertEquals( - "Username and Password not accepted. " + - "Learn more at http://support.google.com/mail/bin/answer.py?answer=14257 hx9sm5317360pbc.68", - e.getMessage()); - } - - InOrder inOrder = inOrder(oAuth2TokenProvider); - inOrder.verify(oAuth2TokenProvider).getToken(eq(USERNAME), anyLong()); - inOrder.verify(oAuth2TokenProvider).invalidateToken(USERNAME); - server.verifyConnectionClosed(); - server.verifyInteractionCompleted(); - } - - @Test - public void open_withManyExtensions_shouldParseAll() throws Exception { - MockSmtpServer server = new MockSmtpServer(); - server.output("220 smtp.gmail.com ESMTP x25sm19117693wrx.27 - gsmtp"); - server.expect("EHLO [127.0.0.1]"); - server.output("250-smtp.gmail.com at your service, [86.147.34.216]"); - server.output("250-SIZE 35882577"); - server.output("250-8BITMIME"); - server.output("250-AUTH LOGIN PLAIN XOAUTH2 PLAIN-CLIENTTOKEN OAUTHBEARER XOAUTH"); - server.output("250-ENHANCEDSTATUSCODES"); - server.output("250-PIPELINING"); - server.output("250-CHUNKING"); - server.output("250 SMTPUTF8"); - server.expect("AUTH XOAUTH2 dXNlcj11c2VyAWF1dGg9QmVhcmVyIG9sZFRva2VuAQE="); - server.output("235 2.7.0 Authentication successful"); - SmtpTransport transport = startServerAndCreateSmtpTransport(server, AuthType.XOAUTH2, ConnectionSecurity.NONE); - - transport.open(); - - server.verifyConnectionStillOpen(); - server.verifyInteractionCompleted(); - } - - @Test - public void sendMessage_withoutAddressToSendTo_shouldNotOpenConnection() throws Exception { - MimeMessage message = new MimeMessage(); - MockSmtpServer server = createServerAndSetupForPlainAuthentication(); - SmtpTransport transport = startServerAndCreateSmtpTransport(server); - - transport.sendMessage(message); - - server.verifyConnectionNeverCreated(); - } - - @Test - public void sendMessage_withSingleRecipient() throws Exception { - Message message = getDefaultMessage(); - MockSmtpServer server = createServerAndSetupForPlainAuthentication(); - server.expect("MAIL FROM:"); - server.output("250 OK"); - server.expect("RCPT TO:"); - server.output("250 OK"); - server.expect("DATA"); - server.output("354 End data with ."); - server.expect("[message data]"); - server.expect("."); - server.output("250 OK: queued as 12345"); - server.expect("QUIT"); - server.output("221 BYE"); - server.closeConnection(); - SmtpTransport transport = startServerAndCreateSmtpTransport(server); - - transport.sendMessage(message); - - server.verifyConnectionClosed(); - server.verifyInteractionCompleted(); - } - - @Test - public void sendMessage_with8BitEncoding() throws Exception { - Message message = getDefaultMessage(); - MockSmtpServer server = createServerAndSetupForPlainAuthentication("8BITMIME"); - server.expect("MAIL FROM: BODY=8BITMIME"); - server.output("250 OK"); - server.expect("RCPT TO:"); - server.output("250 OK"); - server.expect("DATA"); - server.output("354 End data with ."); - server.expect("[message data]"); - server.expect("."); - server.output("250 OK: queued as 12345"); - server.expect("QUIT"); - server.output("221 BYE"); - server.closeConnection(); - SmtpTransport transport = startServerAndCreateSmtpTransport(server); - - transport.sendMessage(message); - - server.verifyConnectionClosed(); - server.verifyInteractionCompleted(); - } - - @Test - public void sendMessage_with8BitEncodingExtensionNotCaseSensitive() throws Exception { - Message message = getDefaultMessage(); - MockSmtpServer server = createServerAndSetupForPlainAuthentication("8bitmime"); - server.expect("MAIL FROM: BODY=8BITMIME"); - server.output("250 OK"); - server.expect("RCPT TO:"); - server.output("250 OK"); - server.expect("DATA"); - server.output("354 End data with ."); - server.expect("[message data]"); - server.expect("."); - server.output("250 OK: queued as 12345"); - server.expect("QUIT"); - server.output("221 BYE"); - server.closeConnection(); - SmtpTransport transport = startServerAndCreateSmtpTransport(server); - - transport.sendMessage(message); - - server.verifyConnectionClosed(); - server.verifyInteractionCompleted(); - } - - @Test - public void sendMessage_withMessageTooLarge_shouldThrow() throws Exception { - Message message = getDefaultMessageBuilder() - .setHasAttachments(true) - .messageSize(1234L) - .build(); - MockSmtpServer server = createServerAndSetupForPlainAuthentication("SIZE 1000"); - SmtpTransport transport = startServerAndCreateSmtpTransport(server); + transport.open() + fail("Exception expected") + } catch (e: AuthenticationFailedException) { + assertThat(e).hasMessageThat().isEqualTo( + "Username and Password not accepted. " + + "Learn more at http://support.google.com/mail/bin/answer.py?answer=14257 hx9sm5317360pbc.68" + ) + } - try { - transport.sendMessage(message); - fail("Expected message too large error"); - } catch (MessagingException e) { - assertTrue(e.isPermanentFailure()); - assertEquals("Message too large for server", e.getMessage()); - } - - //FIXME: Make sure connection was closed - //server.verifyConnectionClosed(); - } - - @Test - public void sendMessage_withNegativeReply_shouldThrow() throws Exception { - Message message = getDefaultMessage(); - MockSmtpServer server = createServerAndSetupForPlainAuthentication(); - server.expect("MAIL FROM:"); - server.output("250 OK"); - server.expect("RCPT TO:"); - server.output("250 OK"); - server.expect("DATA"); - server.output("354 End data with ."); - server.expect("[message data]"); - server.expect("."); - server.output("421 4.7.0 Temporary system problem"); - server.expect("QUIT"); - server.output("221 BYE"); - server.closeConnection(); - SmtpTransport transport = startServerAndCreateSmtpTransport(server); + inOrder(oAuth2TokenProvider) { + verify(oAuth2TokenProvider).getToken(eq(USERNAME), anyLong()) + verify(oAuth2TokenProvider).invalidateToken(USERNAME) + } + server.verifyConnectionClosed() + server.verifyInteractionCompleted() + } + + @Test + fun `open() with many extensions should parse all`() { + val server = MockSmtpServer().apply { + output("220 smtp.gmail.com ESMTP x25sm19117693wrx.27 - gsmtp") + expect("EHLO [127.0.0.1]") + output("250-smtp.gmail.com at your service, [86.147.34.216]") + output("250-SIZE 35882577") + output("250-8BITMIME") + output("250-AUTH LOGIN PLAIN XOAUTH2 PLAIN-CLIENTTOKEN OAUTHBEARER XOAUTH") + output("250-ENHANCEDSTATUSCODES") + output("250-PIPELINING") + output("250-CHUNKING") + output("250 SMTPUTF8") + expect("AUTH XOAUTH2 dXNlcj11c2VyAWF1dGg9QmVhcmVyIG9sZFRva2VuAQE=") + output("235 2.7.0 Authentication successful") + } + val transport = startServerAndCreateSmtpTransport(server, authenticationType = AuthType.XOAUTH2) + + transport.open() + + server.verifyConnectionStillOpen() + server.verifyInteractionCompleted() + } + + @Test + fun `sendMessage() without address to send to should not open connection`() { + val message = MimeMessage() + val server = createServerAndSetupForPlainAuthentication() + val transport = startServerAndCreateSmtpTransport(server) + + transport.sendMessage(message) + + server.verifyConnectionNeverCreated() + } + + @Test + fun `sendMessage() with single recipient`() { + val message = createDefaultMessage() + val server = createServerAndSetupForPlainAuthentication().apply { + expect("MAIL FROM:") + output("250 OK") + expect("RCPT TO:") + output("250 OK") + expect("DATA") + output("354 End data with .") + expect("[message data]") + expect(".") + output("250 OK: queued as 12345") + expect("QUIT") + output("221 BYE") + closeConnection() + } + val transport = startServerAndCreateSmtpTransport(server) + + transport.sendMessage(message) + + server.verifyConnectionClosed() + server.verifyInteractionCompleted() + } + + @Test + fun `sendMessage() with 8-bit encoding`() { + val message = createDefaultMessage() + val server = createServerAndSetupForPlainAuthentication("8BITMIME").apply { + expect("MAIL FROM: BODY=8BITMIME") + output("250 OK") + expect("RCPT TO:") + output("250 OK") + expect("DATA") + output("354 End data with .") + expect("[message data]") + expect(".") + output("250 OK: queued as 12345") + expect("QUIT") + output("221 BYE") + closeConnection() + } + val transport = startServerAndCreateSmtpTransport(server) + + transport.sendMessage(message) + + server.verifyConnectionClosed() + server.verifyInteractionCompleted() + } + + @Test + fun `sendMessage() with 8-bit encoding extension not case-sensitive`() { + val message = createDefaultMessage() + val server = createServerAndSetupForPlainAuthentication("8bitmime").apply { + expect("MAIL FROM: BODY=8BITMIME") + output("250 OK") + expect("RCPT TO:") + output("250 OK") + expect("DATA") + output("354 End data with .") + expect("[message data]") + expect(".") + output("250 OK: queued as 12345") + expect("QUIT") + output("221 BYE") + closeConnection() + } + val transport = startServerAndCreateSmtpTransport(server) + + transport.sendMessage(message) + + server.verifyConnectionClosed() + server.verifyInteractionCompleted() + } + + @Test + fun `sendMessage() with message too large should throw`() { + val message = createDefaultMessageBuilder() + .setHasAttachments(true) + .messageSize(1234L) + .build() + val server = createServerAndSetupForPlainAuthentication("SIZE 1000") + val transport = startServerAndCreateSmtpTransport(server) try { - transport.sendMessage(message); - fail("Expected exception"); - } catch (NegativeSmtpReplyException e) { - assertEquals(421, e.getReplyCode()); - assertEquals("4.7.0 Temporary system problem", e.getReplyText()); - } - - server.verifyConnectionClosed(); - server.verifyInteractionCompleted(); - } - - @Test - public void sendMessage_withPipelining() throws Exception { - Message message = getDefaultMessage(); - MockSmtpServer server = createServerAndSetupForPlainAuthentication("PIPELINING"); - server.expect("MAIL FROM:"); - server.expect("RCPT TO:"); - server.output("250 OK"); - server.output("250 OK"); - server.expect("DATA"); - server.output("354 End data with ."); - server.expect("[message data]"); - server.expect("."); - server.output("250 OK: queued as 12345"); - server.expect("QUIT"); - server.output("221 BYE"); - server.closeConnection(); - SmtpTransport transport = startServerAndCreateSmtpTransport(server); - - transport.sendMessage(message); - - server.verifyConnectionClosed(); - server.verifyInteractionCompleted(); - } - - @Test - public void sendMessage_withoutPipelining() throws Exception { - Message message = getDefaultMessage(); - MockSmtpServer server = createServerAndSetupForPlainAuthentication(); - server.expect("MAIL FROM:"); - server.output("250 OK"); - server.expect("RCPT TO:"); - server.output("250 OK"); - server.expect("DATA"); - server.output("354 End data with ."); - server.expect("[message data]"); - server.expect("."); - server.output("250 OK: queued as 12345"); - server.expect("QUIT"); - server.output("221 BYE"); - server.closeConnection(); - SmtpTransport transport = startServerAndCreateSmtpTransport(server); - - transport.sendMessage(message); - - server.verifyConnectionClosed(); - server.verifyInteractionCompleted(); - } - - @Test - public void sendMessagePipelining_withNegativeReply() throws Exception { - Message message = getDefaultMessage(); - MockSmtpServer server = createServerAndSetupForPlainAuthentication("PIPELINING"); - server.expect("MAIL FROM:"); - server.expect("RCPT TO:"); - server.output("250 OK"); - server.output("550 remote mail to not allowed"); - server.expect("QUIT"); - server.output("221 BYE"); - server.closeConnection(); - SmtpTransport transport = startServerAndCreateSmtpTransport(server); + transport.sendMessage(message) + fail("Expected message too large error") + } catch (e: MessagingException) { + assertThat(e.isPermanentFailure).isTrue() + assertThat(e).hasMessageThat().isEqualTo("Message too large for server") + } + + // FIXME: Make sure connection was closed + // server.verifyConnectionClosed(); + } + + @Test + fun `sendMessage() with negative reply should throw`() { + val message = createDefaultMessage() + val server = createServerAndSetupForPlainAuthentication().apply { + expect("MAIL FROM:") + output("250 OK") + expect("RCPT TO:") + output("250 OK") + expect("DATA") + output("354 End data with .") + expect("[message data]") + expect(".") + output("421 4.7.0 Temporary system problem") + expect("QUIT") + output("221 BYE") + closeConnection() + } + val transport = startServerAndCreateSmtpTransport(server) try { - transport.sendMessage(message); - fail("Expected exception"); - } catch (NegativeSmtpReplyException e) { - assertEquals(550, e.getReplyCode()); - assertEquals("remote mail to not allowed", e.getReplyText()); + transport.sendMessage(message) + fail("Expected exception") + } catch (e: NegativeSmtpReplyException) { + assertThat(e.replyCode).isEqualTo(421) + assertThat(e.replyText).isEqualTo("4.7.0 Temporary system problem") } + server.verifyConnectionClosed() + server.verifyInteractionCompleted() + } + + @Test + fun `sendMessage() with pipelining`() { + val message = createDefaultMessage() + val server = createServerAndSetupForPlainAuthentication("PIPELINING").apply { + expect("MAIL FROM:") + expect("RCPT TO:") + output("250 OK") + output("250 OK") + expect("DATA") + output("354 End data with .") + expect("[message data]") + expect(".") + output("250 OK: queued as 12345") + expect("QUIT") + output("221 BYE") + closeConnection() + } + val transport = startServerAndCreateSmtpTransport(server) + + transport.sendMessage(message) + + server.verifyConnectionClosed() + server.verifyInteractionCompleted() + } + + @Test + fun `sendMessage() without pipelining`() { + val message = createDefaultMessage() + val server = createServerAndSetupForPlainAuthentication().apply { + expect("MAIL FROM:") + output("250 OK") + expect("RCPT TO:") + output("250 OK") + expect("DATA") + output("354 End data with .") + expect("[message data]") + expect(".") + output("250 OK: queued as 12345") + expect("QUIT") + output("221 BYE") + closeConnection() + } + val transport = startServerAndCreateSmtpTransport(server) - server.verifyConnectionClosed(); - server.verifyInteractionCompleted(); + transport.sendMessage(message) + + server.verifyConnectionClosed() + server.verifyInteractionCompleted() } @Test - public void sendMessagePipelining_without354ReplyforData_shouldThrow() throws Exception { - Message message = getDefaultMessage(); - MockSmtpServer server = createServerAndSetupForPlainAuthentication("PIPELINING"); - server.expect("MAIL FROM:"); - server.expect("RCPT TO:"); - server.output("250 OK"); - server.output("550 remote mail to not allowed"); - server.expect("QUIT"); - server.output("221 BYE"); - server.closeConnection(); - SmtpTransport transport = startServerAndCreateSmtpTransport(server); + fun `sendMessage() with pipelining and negative reply`() { + val message = createDefaultMessage() + val server = createServerAndSetupForPlainAuthentication("PIPELINING").apply { + expect("MAIL FROM:") + expect("RCPT TO:") + output("250 OK") + output("550 remote mail to not allowed") + expect("QUIT") + output("221 BYE") + closeConnection() + } + val transport = startServerAndCreateSmtpTransport(server) try { - transport.sendMessage(message); - fail("Expected exception"); - } catch (NegativeSmtpReplyException e) { - assertEquals(550, e.getReplyCode()); - assertEquals("remote mail to not allowed", e.getReplyText()); + transport.sendMessage(message) + fail("Expected exception") + } catch (e: NegativeSmtpReplyException) { + assertThat(e.replyCode).isEqualTo(550) + assertThat(e.replyText).isEqualTo("remote mail to not allowed") } - server.verifyConnectionClosed(); - server.verifyInteractionCompleted(); + server.verifyConnectionClosed() + server.verifyInteractionCompleted() } @Test - public void sendMessagePipelining_with250and550ReplyforRecipients_shouldThrowFirst() throws Exception { - Message message = getMessageWithTwoRecipients(); - MockSmtpServer server = createServerAndSetupForPlainAuthentication("PIPELINING"); - server.expect("MAIL FROM:"); - server.expect("RCPT TO:"); - server.expect("RCPT TO:"); - server.output("250 OK"); - server.output("550 remote mail to not allowed"); - server.output("550 remote mail to not allowed"); - server.expect("QUIT"); - server.output("221 BYE"); - server.closeConnection(); - SmtpTransport transport = startServerAndCreateSmtpTransport(server); + fun `sendMessage() with pipelining and missing 354 reply for DATA should throw`() { + val message = createDefaultMessage() + val server = createServerAndSetupForPlainAuthentication("PIPELINING") + server.expect("MAIL FROM:") + server.expect("RCPT TO:") + server.output("250 OK") + server.output("550 remote mail to not allowed") + server.expect("QUIT") + server.output("221 BYE") + server.closeConnection() + val transport = startServerAndCreateSmtpTransport(server) try { - transport.sendMessage(message); - fail("Expected exception"); - } catch (NegativeSmtpReplyException e) { - assertEquals(550, e.getReplyCode()); - assertEquals("remote mail to not allowed", e.getReplyText()); + transport.sendMessage(message) + fail("Expected exception") + } catch (e: NegativeSmtpReplyException) { + assertThat(e.replyCode).isEqualTo(550) + assertThat(e.replyText).isEqualTo("remote mail to not allowed") } - server.verifyConnectionClosed(); - server.verifyInteractionCompleted(); + server.verifyConnectionClosed() + server.verifyInteractionCompleted() } @Test - public void sendMessagePipelining_with250and550ReplyforRecipientsAnd250ForMessage_shouldThrow() throws Exception { - Message message = getMessageWithTwoRecipients(); - MockSmtpServer server = createServerAndSetupForPlainAuthentication("PIPELINING"); - server.expect("MAIL FROM:"); - server.expect("RCPT TO:"); - server.expect("RCPT TO:"); - server.output("250 OK"); - server.output("250 OK"); - server.output("550 remote mail to not allowed"); - server.expect("QUIT"); - server.output("221 BYE"); - server.closeConnection(); - SmtpTransport transport = startServerAndCreateSmtpTransport(server); + fun `sendMessage() with pipelining and two 550 replies for recipients should include first error in exception`() { + val message = createMessageWithTwoRecipients() + val server = createServerAndSetupForPlainAuthentication("PIPELINING").apply { + expect("MAIL FROM:") + expect("RCPT TO:") + expect("RCPT TO:") + output("250 OK") + output("550 remote mail to not allowed") + output("550 remote mail to not allowed") + expect("QUIT") + output("221 BYE") + closeConnection() + } + val transport = startServerAndCreateSmtpTransport(server) try { - transport.sendMessage(message); - fail("Expected exception"); - } catch (NegativeSmtpReplyException e) { - assertEquals(550, e.getReplyCode()); - assertEquals("remote mail to not allowed", e.getReplyText()); + transport.sendMessage(message) + fail("Expected exception") + } catch (e: NegativeSmtpReplyException) { + assertThat(e.replyCode).isEqualTo(550) + assertThat(e.replyText).isEqualTo("remote mail to not allowed") } - server.verifyConnectionClosed(); - server.verifyInteractionCompleted(); + server.verifyConnectionClosed() + server.verifyInteractionCompleted() } + @Test + fun `sendMessage() with pipelining and both 250 and 550 response for recipients should throw`() { + val message = createMessageWithTwoRecipients() + val server = createServerAndSetupForPlainAuthentication("PIPELINING").apply { + expect("MAIL FROM:") + expect("RCPT TO:") + expect("RCPT TO:") + output("250 OK") + output("250 OK") + output("550 remote mail to not allowed") + expect("QUIT") + output("221 BYE") + closeConnection() + } + val transport = startServerAndCreateSmtpTransport(server) - private SmtpTransport startServerAndCreateSmtpTransport(MockSmtpServer server) throws Exception { - return startServerAndCreateSmtpTransport(server, AuthType.PLAIN, ConnectionSecurity.NONE); - } + try { + transport.sendMessage(message) + fail("Expected exception") + } catch (e: NegativeSmtpReplyException) { + assertThat(e.replyCode).isEqualTo(550) + assertThat(e.replyText).isEqualTo("remote mail to not allowed") + } - private SmtpTransport startServerAndCreateSmtpTransportWithoutPassword(MockSmtpServer server) throws Exception { - return startServerAndCreateSmtpTransport(server, AuthType.PLAIN, ConnectionSecurity.NONE, null); + server.verifyConnectionClosed() + server.verifyInteractionCompleted() } - private SmtpTransport startServerAndCreateSmtpTransport(MockSmtpServer server, AuthType authenticationType, - ConnectionSecurity connectionSecurity) throws Exception { - return startServerAndCreateSmtpTransport(server, authenticationType, connectionSecurity, PASSWORD); + private fun startServerAndCreateSmtpTransportWithoutPassword(server: MockSmtpServer): SmtpTransport { + return startServerAndCreateSmtpTransport(server, AuthType.PLAIN, ConnectionSecurity.NONE, null) } - private SmtpTransport startServerAndCreateSmtpTransport(MockSmtpServer server, AuthType authenticationType, - ConnectionSecurity connectionSecurity, String password) - throws Exception { - server.start(); + private fun startServerAndCreateSmtpTransport( + server: MockSmtpServer, + authenticationType: AuthType = AuthType.PLAIN, + connectionSecurity: ConnectionSecurity = ConnectionSecurity.NONE, + password: String? = PASSWORD + ): SmtpTransport { + server.start() + val host = server.host + val port = server.port + val serverSettings = ServerSettings( + "smtp", + host, + port, + connectionSecurity, + authenticationType, + USERNAME, + password, + CLIENT_CERTIFICATE_ALIAS + ) - String host = server.getHost(); - int port = server.getPort(); - ServerSettings serverSettings = new ServerSettings( - "smtp", - host, - port, - connectionSecurity, - authenticationType, - USERNAME, - password, - CLIENT_CERTIFICATE_ALIAS); + return SmtpTransport(serverSettings, socketFactory, oAuth2TokenProvider) + } - return new SmtpTransport(serverSettings, socketFactory, oAuth2TokenProvider); + private fun createDefaultMessageBuilder(): TestMessageBuilder { + return TestMessageBuilder() + .from("user@localhost") + .to("user2@localhost") } - private TestMessageBuilder getDefaultMessageBuilder() { - return new TestMessageBuilder() - .from("user@localhost") - .to("user2@localhost"); + private fun createDefaultMessage(): Message { + return createDefaultMessageBuilder().build() } - private Message getDefaultMessage() { - return getDefaultMessageBuilder().build(); + private fun createMessageWithTwoRecipients(): Message { + return TestMessageBuilder() + .from("user@localhost") + .to("user2@localhost", "user3@localhost") + .build() } - private Message getMessageWithTwoRecipients() { - return new TestMessageBuilder() - .from("user@localhost") - .to("user2@localhost", "user3@localhost") - .build(); + private fun createServerAndSetupForPlainAuthentication(vararg extensions: String): MockSmtpServer { + return MockSmtpServer().apply { + output("220 localhost Simple Mail Transfer Service Ready") + expect("EHLO [127.0.0.1]") + output("250-localhost Hello client.localhost") + + for (extension in extensions) { + output("250-$extension") + } + + output("250 AUTH LOGIN PLAIN CRAM-MD5") + expect("AUTH PLAIN AHVzZXIAcGFzc3dvcmQ=") + output("235 2.7.0 Authentication successful") + } } - private MockSmtpServer createServerAndSetupForPlainAuthentication(String... extensions) { - MockSmtpServer server = new MockSmtpServer(); - - server.output("220 localhost Simple Mail Transfer Service Ready"); - server.expect("EHLO [127.0.0.1]"); - server.output("250-localhost Hello client.localhost"); - - for (String extension : extensions) { - server.output("250-" + extension); + private fun createMockOAuth2TokenProvider(): OAuth2TokenProvider { + return mock { + on { getToken(eq(USERNAME), anyLong()) } doReturn "oldToken" doReturn "newToken" } - - server.output("250 AUTH LOGIN PLAIN CRAM-MD5"); - server.expect("AUTH PLAIN AHVzZXIAcGFzc3dvcmQ="); - server.output("235 2.7.0 Authentication successful"); - - return server; } } -- GitLab From 2f258d6886f5c7d4d1130e0a3248f8757ef116e8 Mon Sep 17 00:00:00 2001 From: cketti Date: Thu, 14 Apr 2022 18:30:27 +0200 Subject: [PATCH 16/75] Rename .java to .kt --- .../mail/transport/smtp/{SmtpTransport.java => SmtpTransport.kt} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename mail/protocols/smtp/src/main/java/com/fsck/k9/mail/transport/smtp/{SmtpTransport.java => SmtpTransport.kt} (100%) diff --git a/mail/protocols/smtp/src/main/java/com/fsck/k9/mail/transport/smtp/SmtpTransport.java b/mail/protocols/smtp/src/main/java/com/fsck/k9/mail/transport/smtp/SmtpTransport.kt similarity index 100% rename from mail/protocols/smtp/src/main/java/com/fsck/k9/mail/transport/smtp/SmtpTransport.java rename to mail/protocols/smtp/src/main/java/com/fsck/k9/mail/transport/smtp/SmtpTransport.kt -- GitLab From 56da545493cd53bc89b5ddec4c2086ab53284710 Mon Sep 17 00:00:00 2001 From: cketti Date: Thu, 14 Apr 2022 18:30:27 +0200 Subject: [PATCH 17/75] Convert `SmtpTransport` to Kotlin --- .../k9/mail/transport/smtp/SmtpTransport.kt | 906 ++++++++---------- .../mail/transport/smtp/SmtpTransportTest.kt | 2 +- 2 files changed, 415 insertions(+), 493 deletions(-) diff --git a/mail/protocols/smtp/src/main/java/com/fsck/k9/mail/transport/smtp/SmtpTransport.kt b/mail/protocols/smtp/src/main/java/com/fsck/k9/mail/transport/smtp/SmtpTransport.kt index ff1c232b16..10d31c83ea 100644 --- a/mail/protocols/smtp/src/main/java/com/fsck/k9/mail/transport/smtp/SmtpTransport.kt +++ b/mail/protocols/smtp/src/main/java/com/fsck/k9/mail/transport/smtp/SmtpTransport.kt @@ -1,703 +1,625 @@ - -package com.fsck.k9.mail.transport.smtp; - - -import java.io.BufferedInputStream; -import java.io.BufferedOutputStream; -import java.io.IOException; -import java.io.OutputStream; -import java.net.Inet6Address; -import java.net.InetAddress; -import java.net.InetSocketAddress; -import java.net.Socket; -import java.net.SocketAddress; -import java.net.SocketException; -import java.security.GeneralSecurityException; -import java.util.HashMap; -import java.util.LinkedHashSet; -import java.util.LinkedList; -import java.util.List; -import java.util.Locale; -import java.util.Map; -import java.util.Queue; -import java.util.Set; - -import android.text.TextUtils; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import com.fsck.k9.mail.Address; -import com.fsck.k9.mail.AuthType; -import com.fsck.k9.mail.Authentication; -import com.fsck.k9.mail.AuthenticationFailedException; -import com.fsck.k9.mail.CertificateValidationException; -import com.fsck.k9.mail.ConnectionSecurity; -import com.fsck.k9.mail.K9MailLib; -import com.fsck.k9.mail.Message; -import com.fsck.k9.mail.Message.RecipientType; -import com.fsck.k9.mail.MessagingException; -import com.fsck.k9.mail.ServerSettings; -import com.fsck.k9.mail.Transport; -import com.fsck.k9.mail.filter.Base64; -import com.fsck.k9.mail.filter.EOLConvertingOutputStream; -import com.fsck.k9.mail.filter.LineWrapOutputStream; -import com.fsck.k9.mail.filter.PeekableInputStream; -import com.fsck.k9.mail.filter.SmtpDataStuffing; -import com.fsck.k9.mail.oauth.OAuth2TokenProvider; -import com.fsck.k9.mail.oauth.XOAuth2ChallengeParser; -import com.fsck.k9.mail.ssl.TrustedSocketFactory; -import javax.net.ssl.SSLException; -import org.apache.commons.io.IOUtils; -import timber.log.Timber; - -import static com.fsck.k9.mail.CertificateValidationException.Reason.MissingCapability; -import static com.fsck.k9.mail.K9MailLib.DEBUG_PROTOCOL_SMTP; - -public class SmtpTransport extends Transport { - private static final int SMTP_CONTINUE_REQUEST = 334; - private static final int SMTP_AUTHENTICATION_FAILURE_ERROR_CODE = 535; - - - private final TrustedSocketFactory trustedSocketFactory; - private final OAuth2TokenProvider oauthTokenProvider; - - private final String host; - private final int port; - private final String username; - private final String password; - private final String clientCertificateAlias; - private final AuthType authType; - private final ConnectionSecurity connectionSecurity; - - - private Socket socket; - private PeekableInputStream inputStream; - private OutputStream outputStream; - private SmtpResponseParser responseParser; - - private boolean is8bitEncodingAllowed; - private boolean isEnhancedStatusCodesProvided; - private int largestAcceptableMessage; - private boolean retryXoauthWithNewToken; - private boolean isPipeliningSupported; - - private final SmtpLogger logger = new SmtpLogger() { - @Override - public void log(@NonNull String message, @Nullable Object... args) { - log(null, message, args); - } - - @Override - public boolean isRawProtocolLoggingEnabled() { - return K9MailLib.isDebug(); - } - - @Override - public void log(@Nullable Throwable throwable, @NonNull String message, @Nullable Object... args) { - Timber.v(throwable, message, args); - } - }; - - public SmtpTransport(ServerSettings serverSettings, - TrustedSocketFactory trustedSocketFactory, OAuth2TokenProvider oauthTokenProvider) { - if (!serverSettings.type.equals("smtp")) { - throw new IllegalArgumentException("Expected SMTP StoreConfig!"); +package com.fsck.k9.mail.transport.smtp + +import com.fsck.k9.mail.Address +import com.fsck.k9.mail.AuthType +import com.fsck.k9.mail.Authentication +import com.fsck.k9.mail.AuthenticationFailedException +import com.fsck.k9.mail.CertificateValidationException +import com.fsck.k9.mail.ConnectionSecurity +import com.fsck.k9.mail.K9MailLib +import com.fsck.k9.mail.Message +import com.fsck.k9.mail.Message.RecipientType +import com.fsck.k9.mail.MessagingException +import com.fsck.k9.mail.ServerSettings +import com.fsck.k9.mail.Transport +import com.fsck.k9.mail.filter.Base64 +import com.fsck.k9.mail.filter.EOLConvertingOutputStream +import com.fsck.k9.mail.filter.LineWrapOutputStream +import com.fsck.k9.mail.filter.PeekableInputStream +import com.fsck.k9.mail.filter.SmtpDataStuffing +import com.fsck.k9.mail.oauth.OAuth2TokenProvider +import com.fsck.k9.mail.oauth.XOAuth2ChallengeParser +import com.fsck.k9.mail.ssl.TrustedSocketFactory +import com.fsck.k9.mail.transport.smtp.SmtpHelloResponse.Hello +import java.io.BufferedInputStream +import java.io.BufferedOutputStream +import java.io.IOException +import java.io.OutputStream +import java.net.Inet6Address +import java.net.InetAddress +import java.net.InetSocketAddress +import java.net.Socket +import java.net.SocketException +import java.security.GeneralSecurityException +import java.util.Locale +import javax.net.ssl.SSLException +import org.apache.commons.io.IOUtils +import timber.log.Timber + +private const val SMTP_CONTINUE_REQUEST = 334 +private const val SMTP_AUTHENTICATION_FAILURE_ERROR_CODE = 535 + +class SmtpTransport( + serverSettings: ServerSettings, + private val trustedSocketFactory: TrustedSocketFactory, + private val oauthTokenProvider: OAuth2TokenProvider? +) : Transport() { + private val host = serverSettings.host + private val port = serverSettings.port + private val username = serverSettings.username + private val password = serverSettings.password + private val clientCertificateAlias = serverSettings.clientCertificateAlias + private val authType = serverSettings.authenticationType + private val connectionSecurity = serverSettings.connectionSecurity + + private var socket: Socket? = null + private var inputStream: PeekableInputStream? = null + private var outputStream: OutputStream? = null + private var responseParser: SmtpResponseParser? = null + private var is8bitEncodingAllowed = false + private var isEnhancedStatusCodesProvided = false + private var largestAcceptableMessage = 0 + private var retryXoauthWithNewToken = false + private var isPipeliningSupported = false + + private val logger: SmtpLogger = object : SmtpLogger { + override val isRawProtocolLoggingEnabled: Boolean + get() = K9MailLib.isDebug() + + override fun log(throwable: Throwable?, message: String, vararg args: Any?) { + Timber.v(throwable, message, *args) } + } - host = serverSettings.host; - port = serverSettings.port; - - connectionSecurity = serverSettings.connectionSecurity; - - authType = serverSettings.authenticationType; - username = serverSettings.username; - password = serverSettings.password; - clientCertificateAlias = serverSettings.clientCertificateAlias; - - this.trustedSocketFactory = trustedSocketFactory; - this.oauthTokenProvider = oauthTokenProvider; + init { + require(serverSettings.type == "smtp") { "Expected SMTP ServerSettings!" } } - @Override - public void open() throws MessagingException { + @Throws(MessagingException::class) + override fun open() { try { - boolean secureConnection = false; - InetAddress[] addresses = InetAddress.getAllByName(host); - for (int i = 0; i < addresses.length; i++) { + var secureConnection = false + val addresses = InetAddress.getAllByName(host) + for ((index, address) in addresses.withIndex()) { try { - SocketAddress socketAddress = new InetSocketAddress(addresses[i], port); + val socketAddress = InetSocketAddress(address, port) if (connectionSecurity == ConnectionSecurity.SSL_TLS_REQUIRED) { - socket = trustedSocketFactory.createSocket(null, host, port, clientCertificateAlias); - socket.connect(socketAddress, SOCKET_CONNECT_TIMEOUT); - secureConnection = true; + socket = trustedSocketFactory.createSocket(null, host, port, clientCertificateAlias).also { + it.connect(socketAddress, SOCKET_CONNECT_TIMEOUT) + } + secureConnection = true } else { - socket = new Socket(); - socket.connect(socketAddress, SOCKET_CONNECT_TIMEOUT); + socket = Socket().also { + it.connect(socketAddress, SOCKET_CONNECT_TIMEOUT) + } } - } catch (SocketException e) { - if (i < (addresses.length - 1)) { + } catch (e: SocketException) { + if (index < addresses.lastIndex) { // there are still other addresses for that host to try - continue; + continue } - throw new MessagingException("Cannot connect to host", e); + + throw MessagingException("Cannot connect to host", e) } - break; // connection success + + // connection success + break } - // RFC 1047 - socket.setSoTimeout(SOCKET_READ_TIMEOUT); + val socket = this.socket ?: error("socket == null") - inputStream = new PeekableInputStream(new BufferedInputStream(socket.getInputStream(), 1024)); - responseParser = new SmtpResponseParser(logger, inputStream); - outputStream = new BufferedOutputStream(socket.getOutputStream(), 1024); + // RFC 1047 + socket.soTimeout = SOCKET_READ_TIMEOUT - readGreeting(); + inputStream = PeekableInputStream(BufferedInputStream(socket.getInputStream(), 1024)) + responseParser = SmtpResponseParser(logger, inputStream!!) + outputStream = BufferedOutputStream(socket.getOutputStream(), 1024) - String hostnameToReportInHelo = buildHostnameToReport(); + readGreeting() - Map> extensions = sendHello(hostnameToReportInHelo); + val helloName = buildHostnameToReport() + var extensions = sendHello(helloName) - is8bitEncodingAllowed = extensions.containsKey("8BITMIME"); - isEnhancedStatusCodesProvided = extensions.containsKey("ENHANCEDSTATUSCODES"); - isPipeliningSupported = extensions.containsKey("PIPELINING"); + is8bitEncodingAllowed = extensions.containsKey("8BITMIME") + isEnhancedStatusCodesProvided = extensions.containsKey("ENHANCEDSTATUSCODES") + isPipeliningSupported = extensions.containsKey("PIPELINING") if (connectionSecurity == ConnectionSecurity.STARTTLS_REQUIRED) { if (extensions.containsKey("STARTTLS")) { - executeCommand("STARTTLS"); - - socket = trustedSocketFactory.createSocket( - socket, - host, - port, - clientCertificateAlias); - - inputStream = new PeekableInputStream(new BufferedInputStream(socket.getInputStream(), - 1024)); - responseParser = new SmtpResponseParser(logger, inputStream); - outputStream = new BufferedOutputStream(socket.getOutputStream(), 1024); - /* - * Now resend the EHLO. Required by RFC2487 Sec. 5.2, and more specifically, - * Exim. - */ - extensions = sendHello(hostnameToReportInHelo); - secureConnection = true; + executeCommand("STARTTLS") + + this.socket = trustedSocketFactory.createSocket( + socket, + host, + port, + clientCertificateAlias + ) + inputStream = PeekableInputStream(BufferedInputStream(socket.getInputStream(), 1024)) + responseParser = SmtpResponseParser(logger, inputStream!!) + outputStream = BufferedOutputStream(socket.getOutputStream(), 1024) + + // Now resend the EHLO. Required by RFC2487 Sec. 5.2, and more specifically, Exim. + extensions = sendHello(helloName) + secureConnection = true } else { - /* - * This exception triggers a "Certificate error" - * notification that takes the user to the incoming - * server settings for review. This might be needed if - * the account was configured with an obsolete - * "STARTTLS (if available)" setting. - */ - throw new CertificateValidationException( - "STARTTLS connection security not available"); + // This exception triggers a "Certificate error" notification that takes the user to the incoming + // server settings for review. This might be needed if the account was configured with an obsolete + // "STARTTLS (if available)" setting. + throw CertificateValidationException("STARTTLS connection security not available") } } - boolean authLoginSupported = false; - boolean authPlainSupported = false; - boolean authCramMD5Supported = false; - boolean authExternalSupported = false; - boolean authXoauth2Supported = false; - List saslMech = extensions.get("AUTH"); - if (saslMech != null) { - authLoginSupported = saslMech.contains("LOGIN"); - authPlainSupported = saslMech.contains("PLAIN"); - authCramMD5Supported = saslMech.contains("CRAM-MD5"); - authExternalSupported = saslMech.contains("EXTERNAL"); - authXoauth2Supported = saslMech.contains("XOAUTH2"); + var authLoginSupported = false + var authPlainSupported = false + var authCramMD5Supported = false + var authExternalSupported = false + var authXoauth2Supported = false + val saslMechanisms = extensions["AUTH"] + if (saslMechanisms != null) { + authLoginSupported = saslMechanisms.contains("LOGIN") + authPlainSupported = saslMechanisms.contains("PLAIN") + authCramMD5Supported = saslMechanisms.contains("CRAM-MD5") + authExternalSupported = saslMechanisms.contains("EXTERNAL") + authXoauth2Supported = saslMechanisms.contains("XOAUTH2") } - parseOptionalSizeValue(extensions.get("SIZE")); - - if (!TextUtils.isEmpty(username) - && (!TextUtils.isEmpty(password) || - AuthType.EXTERNAL == authType || - AuthType.XOAUTH2 == authType)) { - - switch (authType) { - - /* - * LOGIN is an obsolete option which is unavailable to users, - * but it still may exist in a user's settings from a previous - * version, or it may have been imported. - */ - case LOGIN: - case PLAIN: + parseOptionalSizeValue(extensions["SIZE"]) + + if ( + username.isNotEmpty() && + (!password.isNullOrEmpty() || AuthType.EXTERNAL == authType || AuthType.XOAUTH2 == authType) + ) { + when (authType) { + AuthType.LOGIN, AuthType.PLAIN -> { // try saslAuthPlain first, because it supports UTF-8 explicitly if (authPlainSupported) { - saslAuthPlain(); + saslAuthPlain() } else if (authLoginSupported) { - saslAuthLogin(); + saslAuthLogin() } else { - throw new MessagingException( - "Authentication methods SASL PLAIN and LOGIN are unavailable."); + throw MessagingException("Authentication methods SASL PLAIN and LOGIN are unavailable.") } - break; - - case CRAM_MD5: + } + AuthType.CRAM_MD5 -> { if (authCramMD5Supported) { - saslAuthCramMD5(); + saslAuthCramMD5() } else { - throw new MessagingException("Authentication method CRAM-MD5 is unavailable."); + throw MessagingException("Authentication method CRAM-MD5 is unavailable.") } - break; - case XOAUTH2: + } + AuthType.XOAUTH2 -> { if (authXoauth2Supported && oauthTokenProvider != null) { - saslXoauth2(); + saslXoauth2() } else { - throw new MessagingException("Authentication method XOAUTH2 is unavailable."); + throw MessagingException("Authentication method XOAUTH2 is unavailable.") } - break; - case EXTERNAL: + } + AuthType.EXTERNAL -> { if (authExternalSupported) { - saslAuthExternal(); + saslAuthExternal() } else { - /* - * Some SMTP servers are known to provide no error - * indication when a client certificate fails to - * validate, other than to not offer the AUTH EXTERNAL - * capability. - * - * So, we treat it is an error to not offer AUTH - * EXTERNAL when using client certificates. That way, the - * user can be notified of a problem during account setup. - */ - throw new CertificateValidationException(MissingCapability); + // Some SMTP servers are known to provide no error indication when a client certificate + // fails to validate, other than to not offer the AUTH EXTERNAL capability. + // So, we treat it is an error to not offer AUTH EXTERNAL when using client certificates. + // That way, the user can be notified of a problem during account setup. + throw CertificateValidationException( + CertificateValidationException.Reason.MissingCapability + ) } - break; - - /* - * AUTOMATIC is an obsolete option which is unavailable to users, - * but it still may exist in a user's settings from a previous - * version, or it may have been imported. - */ - case AUTOMATIC: + } + AuthType.AUTOMATIC -> { if (secureConnection) { // try saslAuthPlain first, because it supports UTF-8 explicitly if (authPlainSupported) { - saslAuthPlain(); + saslAuthPlain() } else if (authLoginSupported) { - saslAuthLogin(); + saslAuthLogin() } else if (authCramMD5Supported) { - saslAuthCramMD5(); + saslAuthCramMD5() } else { - throw new MessagingException("No supported authentication methods available."); + throw MessagingException("No supported authentication methods available.") } } else { if (authCramMD5Supported) { - saslAuthCramMD5(); + saslAuthCramMD5() } else { - /* - * We refuse to insecurely transmit the password - * using the obsolete AUTOMATIC setting because of - * the potential for a MITM attack. Affected users - * must choose a different setting. - */ - throw new MessagingException( - "Update your outgoing server authentication setting. AUTOMATIC auth. is unavailable."); + // We refuse to insecurely transmit the password using the obsolete AUTOMATIC setting + // because of the potential for a MITM attack. Affected users must choose a different + // setting. + throw MessagingException( + "Update your outgoing server authentication setting. " + + "AUTOMATIC authentication is unavailable." + ) } } - break; - - default: - throw new MessagingException( - "Unhandled authentication method found in the server settings (bug)."); + } + else -> { + throw MessagingException("Unhandled authentication method found in server settings (bug).") + } } } - } catch (MessagingException e) { - close(); - throw e; - } catch (SSLException e) { - close(); - throw new CertificateValidationException(e.getMessage(), e); - } catch (GeneralSecurityException gse) { - close(); - throw new MessagingException( - "Unable to open connection to SMTP server due to security error.", gse); - } catch (IOException ioe) { - close(); - throw new MessagingException("Unable to open connection to SMTP server.", ioe); + } catch (e: MessagingException) { + close() + throw e + } catch (e: SSLException) { + close() + throw CertificateValidationException(e.message, e) + } catch (e: GeneralSecurityException) { + close() + throw MessagingException("Unable to open connection to SMTP server due to security error.", e) + } catch (e: IOException) { + close() + throw MessagingException("Unable to open connection to SMTP server.", e) } } - private void readGreeting() { - SmtpResponse smtpResponse = responseParser.readGreeting(); - logResponse(smtpResponse, false); + private fun readGreeting() { + val smtpResponse = responseParser!!.readGreeting() + logResponse(smtpResponse) } - private void logResponse(SmtpResponse smtpResponse, boolean omitText) { + private fun logResponse(smtpResponse: SmtpResponse, omitText: Boolean = false) { if (K9MailLib.isDebug()) { - Timber.v("%s", smtpResponse.toLogString(omitText, "SMTP <<< ")); + Timber.v("%s", smtpResponse.toLogString(omitText, linePrefix = "SMTP <<< ")) } } - private String buildHostnameToReport() { - InetAddress localAddress = socket.getLocalAddress(); + private fun buildHostnameToReport(): String { + val localAddress = socket!!.localAddress - // we use local ip statically for privacy reasons, see https://github.com/k9mail/k-9/pull/3798 - if (localAddress instanceof Inet6Address) { - return "[IPv6:::1]"; + // We use local IP statically for privacy reasons, see https://github.com/k9mail/k-9/pull/3798 + return if (localAddress is Inet6Address) { + "[IPv6:::1]" } else { - return "[127.0.0.1]"; + "[127.0.0.1]" } } - private void parseOptionalSizeValue(List sizeParameters) { - if (sizeParameters != null && sizeParameters.size() >= 1) { - String sizeParameter = sizeParameters.get(0); - try { - largestAcceptableMessage = Integer.parseInt(sizeParameter); - } catch (NumberFormatException e) { - if (K9MailLib.isDebug() && DEBUG_PROTOCOL_SMTP) { - Timber.d(e, "Tried to parse %s and get an int", sizeParameter); + private fun parseOptionalSizeValue(sizeParameters: List?) { + if (sizeParameters != null && sizeParameters.isNotEmpty()) { + val sizeParameter = sizeParameters.first() + val size = sizeParameter.toIntOrNull() + if (size != null) { + largestAcceptableMessage = size + } else { + if (K9MailLib.isDebug() && K9MailLib.DEBUG_PROTOCOL_SMTP) { + Timber.d("SIZE parameter is not a valid integer: %s", sizeParameter) } } } } /** - * Send the client "identity" using the EHLO or HELO command. + * Send the client "identity" using the `EHLO` or `HELO` command. * - *

- * We first try the EHLO command. If the server sends a negative response, it probably doesn't - * support the EHLO command. So we try the older HELO command that all servers need to support. - * And if that fails, too, we pretend everything is fine and continue unimpressed. - *

+ * We first try the EHLO command. If the server sends a negative response, it probably doesn't support the + * `EHLO` command. So we try the older `HELO` command that all servers have to support. And if that fails, too, + * we pretend everything is fine and continue unimpressed. * - * @param host - * The EHLO/HELO parameter as defined by the RFC. + * @param host The EHLO/HELO parameter as defined by the RFC. * - * @return A (possibly empty) {@code Map>} of extensions (upper case) and - * their parameters (possibly empty) as returned by the EHLO command. + * @return A (possibly empty) `Map>` of extensions (upper case) and their parameters + * (possibly empty) as returned by the EHLO command. */ - private Map> sendHello(String host) throws IOException, MessagingException { - writeLine("EHLO " + host, false); - - SmtpHelloResponse helloResponse = responseParser.readHelloResponse(); - logResponse(helloResponse.getResponse(), false); + private fun sendHello(host: String): Map> { + writeLine("EHLO $host") - if (helloResponse instanceof SmtpHelloResponse.Hello) { - SmtpHelloResponse.Hello hello = (SmtpHelloResponse.Hello) helloResponse; + val helloResponse = responseParser!!.readHelloResponse() + logResponse(helloResponse.response) - return hello.getKeywords(); + return if (helloResponse is Hello) { + helloResponse.keywords } else { if (K9MailLib.isDebug()) { - Timber.v("Server doesn't support the EHLO command. Trying HELO..."); + Timber.v("Server doesn't support the EHLO command. Trying HELO...") } try { - executeCommand("HELO %s", host); - } catch (NegativeSmtpReplyException e2) { - Timber.w("Server doesn't support the HELO command. Continuing anyway."); + executeCommand("HELO %s", host) + } catch (e: NegativeSmtpReplyException) { + Timber.w("Server doesn't support the HELO command. Continuing anyway.") } - return new HashMap<>(); + emptyMap() } } - @Override - public void sendMessage(Message message) throws MessagingException { - Set addresses = new LinkedHashSet<>(); - for (Address address : message.getRecipients(RecipientType.TO)) { - addresses.add(address.getAddress()); - } - for (Address address : message.getRecipients(RecipientType.CC)) { - addresses.add(address.getAddress()); - } - for (Address address : message.getRecipients(RecipientType.BCC)) { - addresses.add(address.getAddress()); + @Throws(MessagingException::class) + override fun sendMessage(message: Message) { + val addresses = buildSet { + for (address in message.getRecipients(RecipientType.TO)) { + add(address.address) + } + + for (address in message.getRecipients(RecipientType.CC)) { + add(address.address) + } + + for (address in message.getRecipients(RecipientType.BCC)) { + add(address.address) + } } - message.removeHeader("Bcc"); if (addresses.isEmpty()) { - return; + return } - close(); - open(); + message.removeHeader("Bcc") + + close() + open() - // If the message has attachments and our server has told us about a limit on - // the size of messages, count the message's size before sending it + // If the message has attachments and our server has told us about a limit on the size of messages, count + // the message's size before sending it. if (largestAcceptableMessage > 0 && message.hasAttachments()) { if (message.calculateSize() > largestAcceptableMessage) { - throw new MessagingException("Message too large for server", true); + throw MessagingException("Message too large for server", true) } } - boolean entireMessageSent = false; - + var entireMessageSent = false try { - String mailFrom = constructSmtpMailFromCommand(message.getFrom(), is8bitEncodingAllowed); - + val mailFrom = constructSmtpMailFromCommand(message.from, is8bitEncodingAllowed) if (isPipeliningSupported) { - Queue pipelinedCommands = new LinkedList<>(); - pipelinedCommands.add(mailFrom); + val pipelinedCommands = buildList { + add(mailFrom) - for (String address : addresses) { - pipelinedCommands.add(String.format("RCPT TO:<%s>", address)); + for (address in addresses) { + add(String.format("RCPT TO:<%s>", address)) + } } - executePipelinedCommands(pipelinedCommands); - readPipelinedResponse(pipelinedCommands); + executePipelinedCommands(pipelinedCommands) + readPipelinedResponse(pipelinedCommands) } else { - executeCommand(mailFrom); + executeCommand(mailFrom) - for (String address : addresses) { - executeCommand("RCPT TO:<%s>", address); + for (address in addresses) { + executeCommand("RCPT TO:<%s>", address) } } - executeCommand("DATA"); + executeCommand("DATA") - EOLConvertingOutputStream msgOut = new EOLConvertingOutputStream( - new LineWrapOutputStream(new SmtpDataStuffing(outputStream), 1000)); + val msgOut = EOLConvertingOutputStream( + LineWrapOutputStream( + SmtpDataStuffing(outputStream), 1000 + ) + ) - message.writeTo(msgOut); - msgOut.endWithCrLfAndFlush(); + message.writeTo(msgOut) + msgOut.endWithCrLfAndFlush() - entireMessageSent = true; // After the "\r\n." is attempted, we may have sent the message - executeCommand("."); - } catch (NegativeSmtpReplyException e) { - throw e; - } catch (Exception e) { - throw new MessagingException("Unable to send message", entireMessageSent, e); + // After the "\r\n." is attempted, we may have sent the message + entireMessageSent = true + executeCommand(".") + } catch (e: NegativeSmtpReplyException) { + throw e + } catch (e: Exception) { + throw MessagingException("Unable to send message", entireMessageSent, e) } finally { - close(); + close() } - } - private static String constructSmtpMailFromCommand(Address[] from, boolean is8bitEncodingAllowed) { - String fromAddress = from[0].getAddress(); - if (is8bitEncodingAllowed) { - return String.format("MAIL FROM:<%s> BODY=8BITMIME", fromAddress); + private fun constructSmtpMailFromCommand(from: Array
, is8bitEncodingAllowed: Boolean): String { + val fromAddress = from.first().address + return if (is8bitEncodingAllowed) { + String.format("MAIL FROM:<%s> BODY=8BITMIME", fromAddress) } else { - Timber.d("Server does not support 8bit transfer encoding"); - return String.format("MAIL FROM:<%s>", fromAddress); + Timber.d("Server does not support 8-bit transfer encoding") + String.format("MAIL FROM:<%s>", fromAddress) } } - @Override - public void close() { + override fun close() { try { - executeCommand("QUIT"); - } catch (Exception e) { - // don't care + executeCommand("QUIT") + } catch (ignored: Exception) { } - IOUtils.closeQuietly(inputStream); - IOUtils.closeQuietly(outputStream); - IOUtils.closeQuietly(socket); - inputStream = null; - responseParser = null; - outputStream = null; - socket = null; + + IOUtils.closeQuietly(inputStream) + IOUtils.closeQuietly(outputStream) + IOUtils.closeQuietly(socket) + + inputStream = null + responseParser = null + outputStream = null + socket = null } - private void writeLine(String s, boolean sensitive) throws IOException { - if (K9MailLib.isDebug() && DEBUG_PROTOCOL_SMTP) { - final String commandToLog; - if (sensitive && !K9MailLib.isDebugSensitive()) { - commandToLog = "SMTP >>> *sensitive*"; + private fun writeLine(command: String, sensitive: Boolean = false) { + if (K9MailLib.isDebug() && K9MailLib.DEBUG_PROTOCOL_SMTP) { + val commandToLog = if (sensitive && !K9MailLib.isDebugSensitive()) { + "SMTP >>> *sensitive*" } else { - commandToLog = "SMTP >>> " + s; + "SMTP >>> $command" } - Timber.d(commandToLog); + Timber.d(commandToLog) } - byte[] data = s.concat("\r\n").getBytes(); - - /* - * Important: Send command + CRLF using just one write() call. Using - * multiple calls will likely result in multiple TCP packets and some - * SMTP servers misbehave if CR and LF arrive in separate pakets. - * See issue 799. - */ - outputStream.write(data); - outputStream.flush(); + // Important: Send command + CRLF using just one write() call. Using multiple calls might result in multiple + // TCP packets being sent and some SMTP servers misbehave if CR and LF arrive in separate packets. + // See https://code.google.com/archive/p/k9mail/issues/799 + val data = (command + "\r\n").toByteArray() + outputStream!!.apply { + write(data) + flush() + } } - private SmtpResponse executeSensitiveCommand(String format, Object... args) - throws IOException, MessagingException { - return executeCommand(true, format, args); + private fun executeSensitiveCommand(format: String, vararg args: Any): SmtpResponse { + return executeCommand(sensitive = true, format, *args) } - private SmtpResponse executeCommand(String format, Object... args) throws IOException, MessagingException { - return executeCommand(false, format, args); + private fun executeCommand(format: String, vararg args: Any): SmtpResponse { + return executeCommand(sensitive = false, format, *args) } - private SmtpResponse executeCommand(boolean sensitive, String format, Object... args) - throws IOException, MessagingException { - String command = String.format(Locale.ROOT, format, args); - writeLine(command, sensitive); + private fun executeCommand(sensitive: Boolean, format: String, vararg args: Any): SmtpResponse { + val command = String.format(Locale.ROOT, format, *args) + writeLine(command, sensitive) - SmtpResponse response = responseParser.readResponse(isEnhancedStatusCodesProvided); - logResponse(response, sensitive); + val response = responseParser!!.readResponse(isEnhancedStatusCodesProvided) + logResponse(response, sensitive) - if (response.isNegativeResponse()) { - throw buildNegativeSmtpReplyException(response); + if (response.isNegativeResponse) { + throw buildNegativeSmtpReplyException(response) } - return response; + return response } - private NegativeSmtpReplyException buildNegativeSmtpReplyException(SmtpResponse response) { - int replyCode = response.getReplyCode(); - StatusCode statusCode = response.getStatusCode(); - String replyText = response.getJoinedText(); + private fun buildNegativeSmtpReplyException(response: SmtpResponse): NegativeSmtpReplyException { + val replyCode = response.replyCode + val statusCode = response.statusCode + val replyText = response.joinedText - if (statusCode != null) { - return new EnhancedNegativeSmtpReplyException(replyCode, replyText, statusCode); + return if (statusCode != null) { + EnhancedNegativeSmtpReplyException(replyCode, replyText, statusCode) } else { - return new NegativeSmtpReplyException(replyCode, replyText); + NegativeSmtpReplyException(replyCode, replyText) } } - private void executePipelinedCommands(Queue pipelinedCommands) throws IOException { - for (String command : pipelinedCommands) { - writeLine(command, false); + private fun executePipelinedCommands(pipelinedCommands: List) { + for (command in pipelinedCommands) { + writeLine(command, false) } } - private void readPipelinedResponse(Queue pipelinedCommands) throws IOException, MessagingException { - boolean omitText = false; - MessagingException firstException = null; + private fun readPipelinedResponse(pipelinedCommands: List) { + val responseParser = responseParser!! + var firstException: MessagingException? = null - for (int i = 0, size = pipelinedCommands.size(); i < size; i++) { - SmtpResponse response = responseParser.readResponse(isEnhancedStatusCodesProvided); - logResponse(response, omitText); + repeat(pipelinedCommands.size) { + val response = responseParser.readResponse(isEnhancedStatusCodesProvided) + logResponse(response, omitText = false) - if (response.isNegativeResponse() && firstException == null) { - firstException = buildNegativeSmtpReplyException(response); + if (response.isNegativeResponse && firstException == null) { + firstException = buildNegativeSmtpReplyException(response) } } - if (firstException != null) { - throw firstException; + firstException?.let { + throw it } } - private void saslAuthLogin() throws MessagingException, IOException { + private fun saslAuthLogin() { try { - executeCommand("AUTH LOGIN"); - executeSensitiveCommand(Base64.encode(username)); - executeSensitiveCommand(Base64.encode(password)); - } catch (NegativeSmtpReplyException exception) { - if (exception.getReplyCode() == SMTP_AUTHENTICATION_FAILURE_ERROR_CODE) { - throw new AuthenticationFailedException("AUTH LOGIN failed (" + exception.getMessage() + ")"); + executeCommand("AUTH LOGIN") + executeSensitiveCommand(Base64.encode(username)) + executeSensitiveCommand(Base64.encode(password)) + } catch (exception: NegativeSmtpReplyException) { + if (exception.replyCode == SMTP_AUTHENTICATION_FAILURE_ERROR_CODE) { + throw AuthenticationFailedException("AUTH LOGIN failed (${exception.message})") } else { - throw exception; + throw exception } } } - private void saslAuthPlain() throws MessagingException, IOException { - String data = Base64.encode("\000" + username + "\000" + password); + private fun saslAuthPlain() { + val data = Base64.encode("\u0000" + username + "\u0000" + password) try { - executeSensitiveCommand("AUTH PLAIN %s", data); - } catch (NegativeSmtpReplyException exception) { - if (exception.getReplyCode() == SMTP_AUTHENTICATION_FAILURE_ERROR_CODE) { - throw new AuthenticationFailedException("AUTH PLAIN failed (" - + exception.getMessage() + ")"); + executeSensitiveCommand("AUTH PLAIN %s", data) + } catch (exception: NegativeSmtpReplyException) { + if (exception.replyCode == SMTP_AUTHENTICATION_FAILURE_ERROR_CODE) { + throw AuthenticationFailedException("AUTH PLAIN failed (${exception.message})") } else { - throw exception; + throw exception } } } - private void saslAuthCramMD5() throws MessagingException, IOException { - - List respList = executeCommand("AUTH CRAM-MD5").getTexts(); - if (respList.size() != 1) { - throw new MessagingException("Unable to negotiate CRAM-MD5"); + private fun saslAuthCramMD5() { + val respList = executeCommand("AUTH CRAM-MD5").texts + if (respList.size != 1) { + throw MessagingException("Unable to negotiate CRAM-MD5") } - String b64Nonce = respList.get(0); - String b64CRAMString = Authentication.computeCramMd5(username, password, b64Nonce); - + val b64Nonce = respList[0] + val b64CRAMString = Authentication.computeCramMd5(username, password, b64Nonce) try { - executeSensitiveCommand(b64CRAMString); - } catch (NegativeSmtpReplyException exception) { - if (exception.getReplyCode() == SMTP_AUTHENTICATION_FAILURE_ERROR_CODE) { - throw new AuthenticationFailedException(exception.getMessage(), exception); + executeSensitiveCommand(b64CRAMString) + } catch (exception: NegativeSmtpReplyException) { + if (exception.replyCode == SMTP_AUTHENTICATION_FAILURE_ERROR_CODE) { + throw AuthenticationFailedException(exception.message!!, exception) } else { - throw exception; + throw exception } } } - private void saslXoauth2() throws MessagingException, IOException { - retryXoauthWithNewToken = true; + private fun saslXoauth2() { + retryXoauthWithNewToken = true try { - attemptXoauth2(username); - } catch (NegativeSmtpReplyException negativeResponse) { - if (negativeResponse.getReplyCode() != SMTP_AUTHENTICATION_FAILURE_ERROR_CODE) { - throw negativeResponse; + attemptXoauth2(username) + } catch (negativeResponse: NegativeSmtpReplyException) { + if (negativeResponse.replyCode != SMTP_AUTHENTICATION_FAILURE_ERROR_CODE) { + throw negativeResponse } - oauthTokenProvider.invalidateToken(username); + oauthTokenProvider!!.invalidateToken(username) if (!retryXoauthWithNewToken) { - handlePermanentFailure(negativeResponse); + handlePermanentFailure(negativeResponse) } else { - handleTemporaryFailure(username, negativeResponse); + handleTemporaryFailure(username, negativeResponse) } } } - private void handlePermanentFailure(NegativeSmtpReplyException negativeResponse) throws AuthenticationFailedException { - throw new AuthenticationFailedException(negativeResponse.getMessage(), negativeResponse); + private fun handlePermanentFailure(negativeResponse: NegativeSmtpReplyException): Nothing { + throw AuthenticationFailedException(negativeResponse.message!!, negativeResponse) } - private void handleTemporaryFailure(String username, NegativeSmtpReplyException negativeResponseFromOldToken) - throws IOException, MessagingException { - // Token was invalid + private fun handleTemporaryFailure(username: String, negativeResponseFromOldToken: NegativeSmtpReplyException) { + // Token was invalid. We could avoid this double check if we had a reasonable chance of knowing if a token was + // invalid before use (e.g. due to expiry). But we don't. This is the intended behaviour per AccountManager. + Timber.v(negativeResponseFromOldToken, "Authentication exception, re-trying with new token") - //We could avoid this double check if we had a reasonable chance of knowing - //if a token was invalid before use (e.g. due to expiry). But we don't - //This is the intended behaviour per AccountManager - - Timber.v(negativeResponseFromOldToken, "Authentication exception, re-trying with new token"); try { - attemptXoauth2(username); - } catch (NegativeSmtpReplyException negativeResponseFromNewToken) { - if (negativeResponseFromNewToken.getReplyCode() != SMTP_AUTHENTICATION_FAILURE_ERROR_CODE) { - throw negativeResponseFromNewToken; + attemptXoauth2(username) + } catch (negativeResponseFromNewToken: NegativeSmtpReplyException) { + if (negativeResponseFromNewToken.replyCode != SMTP_AUTHENTICATION_FAILURE_ERROR_CODE) { + throw negativeResponseFromNewToken } - //Okay, we failed on a new token. - //Invalidate the token anyway but assume it's permanent. - Timber.v(negativeResponseFromNewToken, "Authentication exception for new token, permanent error assumed"); - - oauthTokenProvider.invalidateToken(username); + // Okay, we failed on a new token. Invalidate the token anyway but assume it's permanent. + Timber.v(negativeResponseFromNewToken, "Authentication exception for new token, permanent error assumed") - handlePermanentFailure(negativeResponseFromNewToken); + oauthTokenProvider!!.invalidateToken(username) + handlePermanentFailure(negativeResponseFromNewToken) } } - private void attemptXoauth2(String username) throws MessagingException, IOException { - String token = oauthTokenProvider.getToken(username, OAuth2TokenProvider.OAUTH2_TIMEOUT); - String authString = Authentication.computeXoauth(username, token); - SmtpResponse response = executeSensitiveCommand("AUTH XOAUTH2 %s", authString); + private fun attemptXoauth2(username: String) { + val token = oauthTokenProvider!!.getToken(username, OAuth2TokenProvider.OAUTH2_TIMEOUT.toLong()) + val authString = Authentication.computeXoauth(username, token) - if (response.getReplyCode() == SMTP_CONTINUE_REQUEST) { - String replyText = response.getJoinedText(); - retryXoauthWithNewToken = XOAuth2ChallengeParser.shouldRetry(replyText, host); + val response = executeSensitiveCommand("AUTH XOAUTH2 %s", authString) + if (response.replyCode == SMTP_CONTINUE_REQUEST) { + val replyText = response.joinedText + retryXoauthWithNewToken = XOAuth2ChallengeParser.shouldRetry(replyText, host) - //Per Google spec, respond to challenge with empty response - executeCommand(""); + // Per Google spec, respond to challenge with empty response + executeCommand("") } } - private void saslAuthExternal() throws MessagingException, IOException { - executeCommand("AUTH EXTERNAL %s", Base64.encode(username)); + private fun saslAuthExternal() { + executeCommand("AUTH EXTERNAL %s", Base64.encode(username)) } - public void checkSettings() throws MessagingException { - close(); + @Throws(MessagingException::class) + fun checkSettings() { + close() + try { - open(); + open() } finally { - close(); + close() } } } diff --git a/mail/protocols/smtp/src/test/java/com/fsck/k9/mail/transport/smtp/SmtpTransportTest.kt b/mail/protocols/smtp/src/test/java/com/fsck/k9/mail/transport/smtp/SmtpTransportTest.kt index a9ef9d8484..6c3e7b196d 100644 --- a/mail/protocols/smtp/src/test/java/com/fsck/k9/mail/transport/smtp/SmtpTransportTest.kt +++ b/mail/protocols/smtp/src/test/java/com/fsck/k9/mail/transport/smtp/SmtpTransportTest.kt @@ -483,7 +483,7 @@ class SmtpTransportTest { fail("Exception expected") } catch (e: MessagingException) { assertThat(e).hasMessageThat().isEqualTo( - "Update your outgoing server authentication setting. AUTOMATIC auth. is unavailable." + "Update your outgoing server authentication setting. AUTOMATIC authentication is unavailable." ) } -- GitLab From 198f04359c427457755e0922ee7a863d77872130 Mon Sep 17 00:00:00 2001 From: cketti Date: Thu, 14 Apr 2022 19:24:44 +0200 Subject: [PATCH 18/75] Rename .java to .kt --- .../transport/smtp/{StatusCodeClass.java => StatusCodeClass.kt} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename mail/protocols/smtp/src/main/java/com/fsck/k9/mail/transport/smtp/{StatusCodeClass.java => StatusCodeClass.kt} (100%) diff --git a/mail/protocols/smtp/src/main/java/com/fsck/k9/mail/transport/smtp/StatusCodeClass.java b/mail/protocols/smtp/src/main/java/com/fsck/k9/mail/transport/smtp/StatusCodeClass.kt similarity index 100% rename from mail/protocols/smtp/src/main/java/com/fsck/k9/mail/transport/smtp/StatusCodeClass.java rename to mail/protocols/smtp/src/main/java/com/fsck/k9/mail/transport/smtp/StatusCodeClass.kt -- GitLab From 0c5a9a678aeca48c914dbcf0dcccded1a46367a8 Mon Sep 17 00:00:00 2001 From: cketti Date: Thu, 14 Apr 2022 19:24:44 +0200 Subject: [PATCH 19/75] Convert `StatusCodeClass` to Kotlin --- .../fsck/k9/mail/transport/smtp/StatusCodeClass.kt | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/mail/protocols/smtp/src/main/java/com/fsck/k9/mail/transport/smtp/StatusCodeClass.kt b/mail/protocols/smtp/src/main/java/com/fsck/k9/mail/transport/smtp/StatusCodeClass.kt index 197f4975e2..f5cd85e96d 100644 --- a/mail/protocols/smtp/src/main/java/com/fsck/k9/mail/transport/smtp/StatusCodeClass.kt +++ b/mail/protocols/smtp/src/main/java/com/fsck/k9/mail/transport/smtp/StatusCodeClass.kt @@ -1,14 +1,7 @@ -package com.fsck.k9.mail.transport.smtp; +package com.fsck.k9.mail.transport.smtp - -enum StatusCodeClass { +internal enum class StatusCodeClass(val codeClass: Int) { SUCCESS(2), PERSISTENT_TRANSIENT_FAILURE(4), PERMANENT_FAILURE(5); - - final int codeClass; - - StatusCodeClass(int codeClass) { - this.codeClass = codeClass; - } } -- GitLab From 0aaa0a9a82476cdbb627dc50610db3d308b10ea9 Mon Sep 17 00:00:00 2001 From: cketti Date: Thu, 14 Apr 2022 19:27:32 +0200 Subject: [PATCH 20/75] Rename .java to .kt --- ...ativeSmtpReplyException.java => NegativeSmtpReplyException.kt} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename mail/protocols/smtp/src/main/java/com/fsck/k9/mail/transport/smtp/{NegativeSmtpReplyException.java => NegativeSmtpReplyException.kt} (100%) diff --git a/mail/protocols/smtp/src/main/java/com/fsck/k9/mail/transport/smtp/NegativeSmtpReplyException.java b/mail/protocols/smtp/src/main/java/com/fsck/k9/mail/transport/smtp/NegativeSmtpReplyException.kt similarity index 100% rename from mail/protocols/smtp/src/main/java/com/fsck/k9/mail/transport/smtp/NegativeSmtpReplyException.java rename to mail/protocols/smtp/src/main/java/com/fsck/k9/mail/transport/smtp/NegativeSmtpReplyException.kt -- GitLab From e9369e1b043851babad3e92aaa80f2a0fb878b0b Mon Sep 17 00:00:00 2001 From: cketti Date: Thu, 14 Apr 2022 19:27:32 +0200 Subject: [PATCH 21/75] Convert `NegativeSmtpReplyException` to Kotlin --- .../smtp/NegativeSmtpReplyException.kt | 49 ++++++------------- 1 file changed, 15 insertions(+), 34 deletions(-) diff --git a/mail/protocols/smtp/src/main/java/com/fsck/k9/mail/transport/smtp/NegativeSmtpReplyException.kt b/mail/protocols/smtp/src/main/java/com/fsck/k9/mail/transport/smtp/NegativeSmtpReplyException.kt index 4a822030fb..fd112c4254 100644 --- a/mail/protocols/smtp/src/main/java/com/fsck/k9/mail/transport/smtp/NegativeSmtpReplyException.kt +++ b/mail/protocols/smtp/src/main/java/com/fsck/k9/mail/transport/smtp/NegativeSmtpReplyException.kt @@ -1,41 +1,22 @@ -package com.fsck.k9.mail.transport.smtp; - - -import android.text.TextUtils; - -import com.fsck.k9.mail.MessagingException; +package com.fsck.k9.mail.transport.smtp +import com.fsck.k9.mail.MessagingException /** * Exception that is thrown when the server sends a negative reply (reply codes 4xx or 5xx). */ -class NegativeSmtpReplyException extends MessagingException { - private static final long serialVersionUID = 8696043577357897135L; - - - private final int replyCode; - private final String replyText; - - - public NegativeSmtpReplyException(int replyCode, String replyText) { - super(buildErrorMessage(replyCode, replyText), isPermanentSmtpError(replyCode)); - this.replyCode = replyCode; - this.replyText = replyText; - } - - private static String buildErrorMessage(int replyCode, String replyText) { - return TextUtils.isEmpty(replyText) ? "Negative SMTP reply: " + replyCode : replyText; - } - - private static boolean isPermanentSmtpError(int replyCode) { - return replyCode >= 500 && replyCode <= 599; - } - - public int getReplyCode() { - return replyCode; - } +open class NegativeSmtpReplyException( + val replyCode: Int, + val replyText: String +) : MessagingException( + buildErrorMessage(replyCode, replyText), + isPermanentSmtpError(replyCode) +) + +private fun buildErrorMessage(replyCode: Int, replyText: String): String { + return replyText.ifEmpty { "Negative SMTP reply: $replyCode" } +} - public String getReplyText() { - return replyText; - } +private fun isPermanentSmtpError(replyCode: Int): Boolean { + return replyCode in 500..599 } -- GitLab From 0934f274b700eb25ac03d323ae7b2fee766db50c Mon Sep 17 00:00:00 2001 From: cketti Date: Thu, 14 Apr 2022 19:35:21 +0200 Subject: [PATCH 22/75] Get rid of `EnhancedNegativeSmtpReplyException` --- .../EnhancedNegativeSmtpReplyException.java | 12 ----- .../mail/transport/smtp/EnhancedStatusCode.kt | 7 +++ .../smtp/NegativeSmtpReplyException.kt | 3 +- .../k9/mail/transport/smtp/SmtpResponse.kt | 26 ++++------- .../mail/transport/smtp/SmtpResponseParser.kt | 26 +++++------ .../k9/mail/transport/smtp/SmtpTransport.kt | 14 ++---- .../k9/mail/transport/smtp/StatusCodeClass.kt | 2 +- .../transport/smtp/SmtpResponseParserTest.kt | 46 +++++++++---------- .../mail/transport/smtp/SmtpResponseTest.kt | 24 +++++----- 9 files changed, 73 insertions(+), 87 deletions(-) delete mode 100644 mail/protocols/smtp/src/main/java/com/fsck/k9/mail/transport/smtp/EnhancedNegativeSmtpReplyException.java create mode 100644 mail/protocols/smtp/src/main/java/com/fsck/k9/mail/transport/smtp/EnhancedStatusCode.kt diff --git a/mail/protocols/smtp/src/main/java/com/fsck/k9/mail/transport/smtp/EnhancedNegativeSmtpReplyException.java b/mail/protocols/smtp/src/main/java/com/fsck/k9/mail/transport/smtp/EnhancedNegativeSmtpReplyException.java deleted file mode 100644 index bb43d59171..0000000000 --- a/mail/protocols/smtp/src/main/java/com/fsck/k9/mail/transport/smtp/EnhancedNegativeSmtpReplyException.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.fsck.k9.mail.transport.smtp; - - -class EnhancedNegativeSmtpReplyException extends NegativeSmtpReplyException { - public final StatusCode statusCode; - - - EnhancedNegativeSmtpReplyException(int replyCode, String replyText, StatusCode statusCode) { - super(replyCode, replyText); - this.statusCode = statusCode; - } -} diff --git a/mail/protocols/smtp/src/main/java/com/fsck/k9/mail/transport/smtp/EnhancedStatusCode.kt b/mail/protocols/smtp/src/main/java/com/fsck/k9/mail/transport/smtp/EnhancedStatusCode.kt new file mode 100644 index 0000000000..34718d335d --- /dev/null +++ b/mail/protocols/smtp/src/main/java/com/fsck/k9/mail/transport/smtp/EnhancedStatusCode.kt @@ -0,0 +1,7 @@ +package com.fsck.k9.mail.transport.smtp + +data class EnhancedStatusCode( + val statusClass: StatusCodeClass, + val subject: Int, + val detail: Int +) diff --git a/mail/protocols/smtp/src/main/java/com/fsck/k9/mail/transport/smtp/NegativeSmtpReplyException.kt b/mail/protocols/smtp/src/main/java/com/fsck/k9/mail/transport/smtp/NegativeSmtpReplyException.kt index fd112c4254..8d430ab7e6 100644 --- a/mail/protocols/smtp/src/main/java/com/fsck/k9/mail/transport/smtp/NegativeSmtpReplyException.kt +++ b/mail/protocols/smtp/src/main/java/com/fsck/k9/mail/transport/smtp/NegativeSmtpReplyException.kt @@ -7,7 +7,8 @@ import com.fsck.k9.mail.MessagingException */ open class NegativeSmtpReplyException( val replyCode: Int, - val replyText: String + val replyText: String, + val enhancedStatusCode: EnhancedStatusCode? = null ) : MessagingException( buildErrorMessage(replyCode, replyText), isPermanentSmtpError(replyCode) diff --git a/mail/protocols/smtp/src/main/java/com/fsck/k9/mail/transport/smtp/SmtpResponse.kt b/mail/protocols/smtp/src/main/java/com/fsck/k9/mail/transport/smtp/SmtpResponse.kt index 0a13b0ea42..4fd8bfaeae 100644 --- a/mail/protocols/smtp/src/main/java/com/fsck/k9/mail/transport/smtp/SmtpResponse.kt +++ b/mail/protocols/smtp/src/main/java/com/fsck/k9/mail/transport/smtp/SmtpResponse.kt @@ -2,7 +2,7 @@ package com.fsck.k9.mail.transport.smtp internal data class SmtpResponse( val replyCode: Int, - val statusCode: StatusCode?, + val enhancedStatusCode: EnhancedStatusCode?, val texts: List ) { val isNegativeResponse = replyCode >= 400 @@ -15,7 +15,7 @@ internal data class SmtpResponse( if (omitText) { append(linePrefix) append(replyCode) - appendIfNotNull(statusCode, prefix = ' ') + appendIfNotNull(enhancedStatusCode, prefix = ' ') if (texts.isNotEmpty()) { append(" [omitted]") } @@ -24,10 +24,10 @@ internal data class SmtpResponse( for (i in 0 until texts.lastIndex) { append(linePrefix) append(replyCode) - if (statusCode == null) { + if (enhancedStatusCode == null) { append('-') } else { - appendIfNotNull(statusCode, prefix = '-') + appendIfNotNull(enhancedStatusCode, prefix = '-') append(' ') } append(texts[i]) @@ -37,7 +37,7 @@ internal data class SmtpResponse( append(linePrefix) append(replyCode) - appendIfNotNull(statusCode, prefix = ' ') + appendIfNotNull(enhancedStatusCode, prefix = ' ') if (texts.isNotEmpty()) { append(' ') append(texts.last()) @@ -46,20 +46,14 @@ internal data class SmtpResponse( } } - private fun StringBuilder.appendIfNotNull(statusCode: StatusCode?, prefix: Char) { - if (statusCode != null) { + private fun StringBuilder.appendIfNotNull(enhancedStatusCode: EnhancedStatusCode?, prefix: Char) { + if (enhancedStatusCode != null) { append(prefix) - append(statusCode.statusClass.codeClass) + append(enhancedStatusCode.statusClass.codeClass) append('.') - append(statusCode.subject) + append(enhancedStatusCode.subject) append('.') - append(statusCode.detail) + append(enhancedStatusCode.detail) } } } - -internal data class StatusCode( - val statusClass: StatusCodeClass, - val subject: Int, - val detail: Int -) diff --git a/mail/protocols/smtp/src/main/java/com/fsck/k9/mail/transport/smtp/SmtpResponseParser.kt b/mail/protocols/smtp/src/main/java/com/fsck/k9/mail/transport/smtp/SmtpResponseParser.kt index f1ff671f96..e67febebd1 100644 --- a/mail/protocols/smtp/src/main/java/com/fsck/k9/mail/transport/smtp/SmtpResponseParser.kt +++ b/mail/protocols/smtp/src/main/java/com/fsck/k9/mail/transport/smtp/SmtpResponseParser.kt @@ -53,7 +53,7 @@ internal class SmtpResponseParser( expect(LF) return SmtpHelloResponse.Hello( - response = SmtpResponse(replyCode, statusCode = null, texts = listOf(text)), + response = SmtpResponse(replyCode, enhancedStatusCode = null, texts = listOf(text)), keywords = emptyMap() ) } @@ -92,7 +92,7 @@ internal class SmtpResponseParser( expect(LF) return SmtpHelloResponse.Hello( - response = SmtpResponse(replyCode, statusCode = null, texts), + response = SmtpResponse(replyCode, enhancedStatusCode = null, texts), keywords = keywords ) } @@ -156,15 +156,15 @@ internal class SmtpResponseParser( private fun readResponseAfterReplyCode(replyCode: Int, enhancedStatusCodes: Boolean): SmtpResponse { val texts = mutableListOf() - var statusCode: StatusCode? = null + var enhancedStatusCode: EnhancedStatusCode? = null var isFirstLine = true - fun BufferedSource.maybeReadAndCompareEnhancedStatusCode(replyCode: Int): StatusCode? { + fun BufferedSource.maybeReadAndCompareEnhancedStatusCode(replyCode: Int): EnhancedStatusCode? { val currentStatusCode = maybeReadEnhancedStatusCode(replyCode) - if (!isFirstLine && statusCode != currentStatusCode) { + if (!isFirstLine && enhancedStatusCode != currentStatusCode) { parserError( "Multi-line response with enhanced status codes not matching: " + - "$statusCode != $currentStatusCode" + "$enhancedStatusCode != $currentStatusCode" ) } isFirstLine = false @@ -178,7 +178,7 @@ internal class SmtpResponseParser( expect(CR) expect(LF) - return SmtpResponse(replyCode, statusCode, texts) + return SmtpResponse(replyCode, enhancedStatusCode, texts) } SPACE -> { expect(SPACE) @@ -186,7 +186,7 @@ internal class SmtpResponseParser( val bufferedSource = readUntilEndOfLine() if (enhancedStatusCodes) { - statusCode = bufferedSource.maybeReadAndCompareEnhancedStatusCode(replyCode) + enhancedStatusCode = bufferedSource.maybeReadAndCompareEnhancedStatusCode(replyCode) } val textString = bufferedSource.readTextString() @@ -197,7 +197,7 @@ internal class SmtpResponseParser( expect(CR) expect(LF) - return SmtpResponse(replyCode, statusCode, texts) + return SmtpResponse(replyCode, enhancedStatusCode, texts) } DASH -> { expect(DASH) @@ -205,7 +205,7 @@ internal class SmtpResponseParser( val bufferedSource = readUntilEndOfLine() if (enhancedStatusCodes) { - statusCode = bufferedSource.maybeReadAndCompareEnhancedStatusCode(replyCode) + enhancedStatusCode = bufferedSource.maybeReadAndCompareEnhancedStatusCode(replyCode) } val textString = bufferedSource.readTextString() @@ -315,7 +315,7 @@ internal class SmtpResponseParser( return text } - private fun BufferedSource.maybeReadEnhancedStatusCode(replyCode: Int): StatusCode? { + private fun BufferedSource.maybeReadEnhancedStatusCode(replyCode: Int): EnhancedStatusCode? { val replyCode1 = replyCode / 100 if (replyCode1 != 2 && replyCode1 != 4 && replyCode1 != 5) return null @@ -333,7 +333,7 @@ internal class SmtpResponseParser( } } - private fun BufferedSource.readEnhancedStatusCode(replyCode1: Int): StatusCode { + private fun BufferedSource.readEnhancedStatusCode(replyCode1: Int): EnhancedStatusCode { val statusClass = readStatusCodeClass(replyCode1) expect(DOT) val subject = readOneToThreeDigitNumber() @@ -342,7 +342,7 @@ internal class SmtpResponseParser( expect(SPACE) - return StatusCode(statusClass, subject, detail) + return EnhancedStatusCode(statusClass, subject, detail) } private fun BufferedSource.readStatusCodeClass(replyCode1: Int): StatusCodeClass { diff --git a/mail/protocols/smtp/src/main/java/com/fsck/k9/mail/transport/smtp/SmtpTransport.kt b/mail/protocols/smtp/src/main/java/com/fsck/k9/mail/transport/smtp/SmtpTransport.kt index 10d31c83ea..fbf7383b3e 100644 --- a/mail/protocols/smtp/src/main/java/com/fsck/k9/mail/transport/smtp/SmtpTransport.kt +++ b/mail/protocols/smtp/src/main/java/com/fsck/k9/mail/transport/smtp/SmtpTransport.kt @@ -470,15 +470,11 @@ class SmtpTransport( } private fun buildNegativeSmtpReplyException(response: SmtpResponse): NegativeSmtpReplyException { - val replyCode = response.replyCode - val statusCode = response.statusCode - val replyText = response.joinedText - - return if (statusCode != null) { - EnhancedNegativeSmtpReplyException(replyCode, replyText, statusCode) - } else { - NegativeSmtpReplyException(replyCode, replyText) - } + return NegativeSmtpReplyException( + replyCode = response.replyCode, + replyText = response.joinedText, + enhancedStatusCode = response.enhancedStatusCode + ) } private fun executePipelinedCommands(pipelinedCommands: List) { diff --git a/mail/protocols/smtp/src/main/java/com/fsck/k9/mail/transport/smtp/StatusCodeClass.kt b/mail/protocols/smtp/src/main/java/com/fsck/k9/mail/transport/smtp/StatusCodeClass.kt index f5cd85e96d..2fe00975b7 100644 --- a/mail/protocols/smtp/src/main/java/com/fsck/k9/mail/transport/smtp/StatusCodeClass.kt +++ b/mail/protocols/smtp/src/main/java/com/fsck/k9/mail/transport/smtp/StatusCodeClass.kt @@ -1,6 +1,6 @@ package com.fsck.k9.mail.transport.smtp -internal enum class StatusCodeClass(val codeClass: Int) { +enum class StatusCodeClass(val codeClass: Int) { SUCCESS(2), PERSISTENT_TRANSIENT_FAILURE(4), PERMANENT_FAILURE(5); diff --git a/mail/protocols/smtp/src/test/java/com/fsck/k9/mail/transport/smtp/SmtpResponseParserTest.kt b/mail/protocols/smtp/src/test/java/com/fsck/k9/mail/transport/smtp/SmtpResponseParserTest.kt index 260b4036f5..e3f349201c 100644 --- a/mail/protocols/smtp/src/test/java/com/fsck/k9/mail/transport/smtp/SmtpResponseParserTest.kt +++ b/mail/protocols/smtp/src/test/java/com/fsck/k9/mail/transport/smtp/SmtpResponseParserTest.kt @@ -17,7 +17,7 @@ class SmtpResponseParserTest { val response = parser.readGreeting() assertThat(response.replyCode).isEqualTo(220) - assertThat(response.statusCode).isNull() + assertThat(response.enhancedStatusCode).isNull() assertThat(response.texts).containsExactly("smtp.domain.example ESMTP ready") assertInputExhausted(input) } @@ -33,7 +33,7 @@ class SmtpResponseParserTest { val response = parser.readGreeting() assertThat(response.replyCode).isEqualTo(220) - assertThat(response.statusCode).isNull() + assertThat(response.enhancedStatusCode).isNull() assertThat(response.texts).containsExactly("Greetings, stranger", "smtp.domain.example ESMTP ready") assertInputExhausted(input) } @@ -244,7 +244,7 @@ class SmtpResponseParserTest { val response = parser.readResponse(enhancedStatusCodes = false) assertThat(response.replyCode).isEqualTo(502) - assertThat(response.statusCode).isNull() + assertThat(response.enhancedStatusCode).isNull() assertThat(response.texts).isEmpty() assertInputExhausted(input) } @@ -257,7 +257,7 @@ class SmtpResponseParserTest { val response = parser.readResponse(enhancedStatusCodes = false) assertThat(response.replyCode).isEqualTo(250) - assertThat(response.statusCode).isNull() + assertThat(response.enhancedStatusCode).isNull() assertThat(response.texts).containsExactly("OK") assertInputExhausted(input) } @@ -270,8 +270,8 @@ class SmtpResponseParserTest { val response = parser.readResponse(enhancedStatusCodes = true) assertThat(response.replyCode).isEqualTo(250) - assertThat(response.statusCode).isEqualTo( - StatusCode(statusClass = StatusCodeClass.SUCCESS, subject = 1, detail = 0) + assertThat(response.enhancedStatusCode).isEqualTo( + EnhancedStatusCode(statusClass = StatusCodeClass.SUCCESS, subject = 1, detail = 0) ) assertThat(response.texts).containsExactly("Originator ok") assertInputExhausted(input) @@ -285,7 +285,7 @@ class SmtpResponseParserTest { val response = parser.readResponse(enhancedStatusCodes = true) assertThat(response.replyCode).isEqualTo(354) - assertThat(response.statusCode).isNull() + assertThat(response.enhancedStatusCode).isNull() assertThat(response.texts).containsExactly("Ok Send data ending with .") assertInputExhausted(input) } @@ -301,7 +301,7 @@ class SmtpResponseParserTest { val response = parser.readResponse(enhancedStatusCodes = false) assertThat(response.replyCode).isEqualTo(500) - assertThat(response.statusCode).isNull() + assertThat(response.enhancedStatusCode).isNull() assertThat(response.texts).containsExactly("Line one", "Line two") assertInputExhausted(input) } @@ -317,7 +317,7 @@ class SmtpResponseParserTest { val response = parser.readResponse(enhancedStatusCodes = false) assertThat(response.replyCode).isEqualTo(500) - assertThat(response.statusCode).isNull() + assertThat(response.enhancedStatusCode).isNull() assertThat(response.texts).containsExactly("", "Line two") assertInputExhausted(input) } @@ -334,7 +334,7 @@ class SmtpResponseParserTest { val response = parser.readResponse(enhancedStatusCodes = false) assertThat(response.replyCode).isEqualTo(500) - assertThat(response.statusCode).isNull() + assertThat(response.enhancedStatusCode).isNull() assertThat(response.texts).containsExactly("Line one", "Line two") assertInputExhausted(input) } @@ -350,8 +350,8 @@ class SmtpResponseParserTest { val response = parser.readResponse(enhancedStatusCodes = true) assertThat(response.replyCode).isEqualTo(250) - assertThat(response.statusCode).isEqualTo( - StatusCode(statusClass = StatusCodeClass.SUCCESS, subject = 1, detail = 0) + assertThat(response.enhancedStatusCode).isEqualTo( + EnhancedStatusCode(statusClass = StatusCodeClass.SUCCESS, subject = 1, detail = 0) ) assertThat(response.texts).containsExactly("Sender ", "OK") assertInputExhausted(input) @@ -368,13 +368,13 @@ class SmtpResponseParserTest { val responseOne = parser.readResponse(enhancedStatusCodes = false) assertThat(responseOne.replyCode).isEqualTo(250) - assertThat(responseOne.statusCode).isNull() + assertThat(responseOne.enhancedStatusCode).isNull() assertThat(responseOne.texts).containsExactly("Sender OK") val responseTwo = parser.readResponse(enhancedStatusCodes = false) assertThat(responseTwo.replyCode).isEqualTo(250) - assertThat(responseTwo.statusCode).isNull() + assertThat(responseTwo.enhancedStatusCode).isNull() assertThat(responseTwo.texts).containsExactly("Recipient OK") assertInputExhausted(input) } @@ -437,7 +437,7 @@ class SmtpResponseParserTest { val response = parser.readResponse(enhancedStatusCodes = false) assertThat(response.replyCode).isEqualTo(280) - assertThat(response.statusCode).isNull() + assertThat(response.enhancedStatusCode).isNull() assertThat(response.texts).containsExactly("Something") assertThat(logger.logEntries).containsExactly( LogEntry(throwable = null, message = "2nd digit of reply code outside of specified range (0..5): 8") @@ -522,7 +522,7 @@ class SmtpResponseParserTest { val response = parser.readResponse(enhancedStatusCodes = false) assertThat(response.replyCode).isEqualTo(200) - assertThat(response.statusCode).isNull() + assertThat(response.enhancedStatusCode).isNull() assertThat(response.texts).isEmpty() assertInputExhausted(input) assertThat(logger.logEntries).containsExactly( @@ -538,7 +538,7 @@ class SmtpResponseParserTest { val response = parser.readResponse(enhancedStatusCodes = false) assertThat(response.replyCode).isEqualTo(200) - assertThat(response.statusCode).isNull() + assertThat(response.enhancedStatusCode).isNull() assertThat(response.texts).containsExactly("über") assertInputExhausted(input) assertThat(logger.logEntries).containsExactly( @@ -554,7 +554,7 @@ class SmtpResponseParserTest { val response = parser.readResponse(enhancedStatusCodes = true) assertThat(response.replyCode).isEqualTo(250) - assertThat(response.statusCode).isNull() + assertThat(response.enhancedStatusCode).isNull() assertThat(response.texts).containsExactly("5.0.0 text") assertInputExhausted(input) assertThat(logger.logEntries).hasSize(1) @@ -572,7 +572,7 @@ class SmtpResponseParserTest { val response = parser.readResponse(enhancedStatusCodes = true) assertThat(response.replyCode).isEqualTo(250) - assertThat(response.statusCode).isNull() + assertThat(response.enhancedStatusCode).isNull() assertThat(response.texts).containsExactly("2.1000.0 Text") assertInputExhausted(input) assertThat(logger.logEntries).hasSize(1) @@ -590,7 +590,7 @@ class SmtpResponseParserTest { val response = parser.readResponse(enhancedStatusCodes = true) assertThat(response.replyCode).isEqualTo(250) - assertThat(response.statusCode).isNull() + assertThat(response.enhancedStatusCode).isNull() assertThat(response.texts).containsExactly("2.0.1000 Text") assertInputExhausted(input) assertThat(logger.logEntries).hasSize(1) @@ -609,7 +609,7 @@ class SmtpResponseParserTest { val response = parser.readResponse(enhancedStatusCodes = true) assertThat(response.replyCode).isEqualTo(550) - assertThat(response.statusCode).isNull() + assertThat(response.enhancedStatusCode).isNull() assertThat(response.texts).containsExactly("Request failed; Mailbox unavailable") assertInputExhausted(input) assertThat(logger.logEntries).hasSize(1) @@ -629,7 +629,7 @@ class SmtpResponseParserTest { assertFailsWithMessage( "Multi-line response with enhanced status codes not matching: " + - "StatusCode(statusClass=PERMANENT_FAILURE, subject=2, detail=1) != null" + "EnhancedStatusCode(statusClass=PERMANENT_FAILURE, subject=2, detail=1) != null" ) { parser.readResponse(enhancedStatusCodes = true) } @@ -646,7 +646,7 @@ class SmtpResponseParserTest { val response = parser.readResponse(enhancedStatusCodes = true) assertThat(response.replyCode).isEqualTo(550) - assertThat(response.statusCode).isNull() + assertThat(response.enhancedStatusCode).isNull() assertThat(response.texts).containsExactly("Request failed", "Mailbox unavailable") assertInputExhausted(input) } diff --git a/mail/protocols/smtp/src/test/java/com/fsck/k9/mail/transport/smtp/SmtpResponseTest.kt b/mail/protocols/smtp/src/test/java/com/fsck/k9/mail/transport/smtp/SmtpResponseTest.kt index 69b29414b4..4cfc6ecf95 100644 --- a/mail/protocols/smtp/src/test/java/com/fsck/k9/mail/transport/smtp/SmtpResponseTest.kt +++ b/mail/protocols/smtp/src/test/java/com/fsck/k9/mail/transport/smtp/SmtpResponseTest.kt @@ -8,7 +8,7 @@ class SmtpResponseTest { fun `log reply code only`() { val response = SmtpResponse( replyCode = 200, - statusCode = null, + enhancedStatusCode = null, texts = emptyList() ) @@ -21,7 +21,7 @@ class SmtpResponseTest { fun `log reply code only with omitText = true`() { val response = SmtpResponse( replyCode = 200, - statusCode = null, + enhancedStatusCode = null, texts = emptyList() ) @@ -34,7 +34,7 @@ class SmtpResponseTest { fun `log reply code and text`() { val response = SmtpResponse( replyCode = 200, - statusCode = null, + enhancedStatusCode = null, texts = listOf("OK") ) @@ -47,7 +47,7 @@ class SmtpResponseTest { fun `log reply code and text with omitText = true`() { val response = SmtpResponse( replyCode = 250, - statusCode = null, + enhancedStatusCode = null, texts = listOf("Sender OK") ) @@ -60,7 +60,7 @@ class SmtpResponseTest { fun `log reply code and status code`() { val response = SmtpResponse( replyCode = 200, - statusCode = StatusCode(statusClass = StatusCodeClass.SUCCESS, subject = 0, detail = 0), + enhancedStatusCode = EnhancedStatusCode(statusClass = StatusCodeClass.SUCCESS, subject = 0, detail = 0), texts = emptyList() ) @@ -73,7 +73,7 @@ class SmtpResponseTest { fun `log reply code and status code with omitText = true`() { val response = SmtpResponse( replyCode = 200, - statusCode = StatusCode(statusClass = StatusCodeClass.SUCCESS, subject = 0, detail = 0), + enhancedStatusCode = EnhancedStatusCode(statusClass = StatusCodeClass.SUCCESS, subject = 0, detail = 0), texts = emptyList() ) @@ -86,7 +86,7 @@ class SmtpResponseTest { fun `log reply code, status code, and text`() { val response = SmtpResponse( replyCode = 200, - statusCode = StatusCode(statusClass = StatusCodeClass.SUCCESS, subject = 0, detail = 0), + enhancedStatusCode = EnhancedStatusCode(statusClass = StatusCodeClass.SUCCESS, subject = 0, detail = 0), texts = listOf("OK") ) @@ -99,7 +99,7 @@ class SmtpResponseTest { fun `log reply code, status code, and text with omitText = true`() { val response = SmtpResponse( replyCode = 200, - statusCode = StatusCode(statusClass = StatusCodeClass.SUCCESS, subject = 0, detail = 0), + enhancedStatusCode = EnhancedStatusCode(statusClass = StatusCodeClass.SUCCESS, subject = 0, detail = 0), texts = listOf("OK") ) @@ -112,7 +112,7 @@ class SmtpResponseTest { fun `log reply code and multi-line text`() { val response = SmtpResponse( replyCode = 250, - statusCode = null, + enhancedStatusCode = null, texts = listOf("Sender ", "OK") ) @@ -130,7 +130,7 @@ class SmtpResponseTest { fun `log reply code and multi-line text with omitText = true`() { val response = SmtpResponse( replyCode = 250, - statusCode = null, + enhancedStatusCode = null, texts = listOf("Sender ", "OK") ) @@ -143,7 +143,7 @@ class SmtpResponseTest { fun `log reply code, status code, and multi-line text`() { val response = SmtpResponse( replyCode = 250, - statusCode = StatusCode(statusClass = StatusCodeClass.SUCCESS, subject = 1, detail = 0), + enhancedStatusCode = EnhancedStatusCode(statusClass = StatusCodeClass.SUCCESS, subject = 1, detail = 0), texts = listOf("Sender ", "OK") ) @@ -161,7 +161,7 @@ class SmtpResponseTest { fun `log reply code, status code, and multi-line text with omitText = true`() { val response = SmtpResponse( replyCode = 250, - statusCode = StatusCode(statusClass = StatusCodeClass.SUCCESS, subject = 1, detail = 0), + enhancedStatusCode = EnhancedStatusCode(statusClass = StatusCodeClass.SUCCESS, subject = 1, detail = 0), texts = listOf("Sender ", "OK") ) -- GitLab From 9b00bccb2cd5aad01132d2207aa07309e1f40d2b Mon Sep 17 00:00:00 2001 From: cketti Date: Wed, 20 Apr 2022 20:31:25 +0200 Subject: [PATCH 23/75] Add tests for `FlowedMessageUtils` --- .../mail/internet/FlowedMessageUtilsTest.kt | 204 ++++++++++++++++++ 1 file changed, 204 insertions(+) create mode 100644 mail/common/src/test/java/com/fsck/k9/mail/internet/FlowedMessageUtilsTest.kt diff --git a/mail/common/src/test/java/com/fsck/k9/mail/internet/FlowedMessageUtilsTest.kt b/mail/common/src/test/java/com/fsck/k9/mail/internet/FlowedMessageUtilsTest.kt new file mode 100644 index 0000000000..5aecb3ad99 --- /dev/null +++ b/mail/common/src/test/java/com/fsck/k9/mail/internet/FlowedMessageUtilsTest.kt @@ -0,0 +1,204 @@ +package com.fsck.k9.mail.internet + +import com.fsck.k9.mail.crlf +import com.google.common.truth.Truth.assertThat +import org.junit.Ignore +import org.junit.Test + +private const val DEL_SP_NO = false +private const val DEL_SP_YES = true + +class FlowedMessageUtilsTest { + @Test + fun `deflow() with simple text`() { + val input = "Text that should be \r\n" + + "displayed on one line" + + val result = FlowedMessageUtils.deflow(input, DEL_SP_NO) + + assertThat(result).isEqualTo("Text that should be displayed on one line") + } + + @Test + fun `deflow() with only some lines ending in space`() { + val input = "Text that \r\n" + + "should be \r\n" + + "displayed on \r\n" + + "one line.\r\n" + + "Text that should retain\r\n" + + "its line break." + + val result = FlowedMessageUtils.deflow(input, DEL_SP_NO) + + assertThat(result).isEqualTo( + """ + Text that should be displayed on one line. + Text that should retain + its line break. + """.trimIndent().crlf() + ) + } + + @Test + fun `deflow() with nothing to do`() { + val input = "Line one\r\nLine two\r\n" + + val result = FlowedMessageUtils.deflow(input, DEL_SP_NO) + + assertThat(result).isEqualTo(input) + } + + @Test + fun `deflow() with quoted text`() { + val input = "On [date], [user] wrote:\r\n" + + "> Text that should be displayed \r\n" + + "> on one line\r\n" + + "\r\n" + + "Some more text that should be \r\n" + + "displayed on one line.\r\n" + + val result = FlowedMessageUtils.deflow(input, DEL_SP_NO) + + assertThat(result).isEqualTo( + """ + |On [date], [user] wrote: + |> Text that should be displayed on one line + | + |Some more text that should be displayed on one line. + | + """.trimMargin().crlf() + ) + } + + @Test + fun `deflow() with quoted text ending in space`() { + val input = "> Quoted text \r\n" + + "Some other text" + + val result = FlowedMessageUtils.deflow(input, DEL_SP_NO) + + assertThat(result).isEqualTo("> Quoted text \r\nSome other text") + } + + @Test + fun `deflow() with quoted text ending in space before quoted text of different quoting depth`() { + val input = ">> Depth 2 \r\n" + + "> Depth 1 \r\n" + + "> is here\r\n" + + "Some other text" + + val result = FlowedMessageUtils.deflow(input, DEL_SP_NO) + + assertThat(result).isEqualTo( + """ + >> Depth 2${" "} + > Depth 1 is here + Some other text + """.trimIndent().crlf() + ) + } + + @Ignore("Fails because of a bug in the code. See GH-6029") + @Test + fun `deflow() with quoted text ending in space followed by empty line`() { + val input = "> Quoted \r\n" + + "\r\n" + + "Text" + + val result = FlowedMessageUtils.deflow(input, DEL_SP_NO) + + assertThat(result).isEqualTo(input) + } + + @Test + fun `deflow() with delSp=true`() { + val input = "Text that is wrapped mid wo \r\nrd" + + val result = FlowedMessageUtils.deflow(input, DEL_SP_YES) + + assertThat(result).isEqualTo("Text that is wrapped mid word") + } + + @Test + fun `deflow() with quoted text and space-stuffing and delSp=true`() { + val input = "> Quoted te \r\n" + + "> xt" + + val result = FlowedMessageUtils.deflow(input, DEL_SP_YES) + + assertThat(result).isEqualTo("> Quoted text") + } + + @Test + fun `deflow() with space-stuffed second line`() { + val input = "Text that should be \r\n" + + " displayed on one line" + + val result = FlowedMessageUtils.deflow(input, DEL_SP_NO) + + assertThat(result).isEqualTo("Text that should be displayed on one line") + } + + @Test + fun `deflow() with only space-stuffing`() { + val input = "Line 1\r\n" + + " Line 2\r\n" + + " Line 3\r\n" + + val result = FlowedMessageUtils.deflow(input, DEL_SP_NO) + + assertThat(result).isEqualTo("Line 1\r\nLine 2\r\nLine 3\r\n") + } + + @Test + fun `deflow() with quoted space-stuffed second line`() { + val input = "> Text that should be \r\n" + + "> displayed on one line" + + val result = FlowedMessageUtils.deflow(input, DEL_SP_NO) + + assertThat(result).isEqualTo("> Text that should be displayed on one line") + } + + @Test + fun `deflow() with text containing signature`() { + val input = "Text that should be \r\n" + + "displayed on one line.\r\n" + + "\r\n" + + "-- \r\n" + + "Signature \r\n" + + "text" + + val result = FlowedMessageUtils.deflow(input, DEL_SP_NO) + + assertThat(result).isEqualTo( + """ + Text that should be displayed on one line. + + --${" "} + Signature text + """.trimIndent().crlf() + ) + } + + @Test + fun `deflow() with quoted text containing signature`() { + val input = "> Text that should be \r\n" + + "> displayed on one line.\r\n" + + "> \r\n" + + "> -- \r\n" + + "> Signature \r\n" + + "> text" + + val result = FlowedMessageUtils.deflow(input, DEL_SP_NO) + + assertThat(result).isEqualTo( + """ + > Text that should be displayed on one line. + >${" "} + > --${" "} + > Signature text + """.trimIndent().crlf() + ) + } +} -- GitLab From 60baf4bd5b906e23e0a5faf482709258160bce56 Mon Sep 17 00:00:00 2001 From: cketti Date: Wed, 20 Apr 2022 22:26:14 +0200 Subject: [PATCH 24/75] Reformat `FlowedMessageUtils.deflow` --- .../k9/mail/internet/FlowedMessageUtils.java | 54 ++++++++++++------- 1 file changed, 36 insertions(+), 18 deletions(-) diff --git a/mail/common/src/main/java/com/fsck/k9/mail/internet/FlowedMessageUtils.java b/mail/common/src/main/java/com/fsck/k9/mail/internet/FlowedMessageUtils.java index 25a10847b4..870665b437 100644 --- a/mail/common/src/main/java/com/fsck/k9/mail/internet/FlowedMessageUtils.java +++ b/mail/common/src/main/java/com/fsck/k9/mail/internet/FlowedMessageUtils.java @@ -46,38 +46,53 @@ public final class FlowedMessageUtils { int actualQuoteDepth = 0; if (line != null && line.length() > 0) { - if (line.equals(RFC2646_SIGNATURE)) + if (line.equals(RFC2646_SIGNATURE)) { // signature handling (the previous line is not flowed) resultLineFlowed = false; - - else if (line.charAt(0) == RFC2646_QUOTE) { + } else if (line.charAt(0) == RFC2646_QUOTE) { // Quote actualQuoteDepth = 1; - while (actualQuoteDepth < line.length() && line.charAt(actualQuoteDepth) == RFC2646_QUOTE) actualQuoteDepth ++; + while (actualQuoteDepth < line.length() && line.charAt(actualQuoteDepth) == RFC2646_QUOTE) { + actualQuoteDepth++; + } // if quote-depth changes wrt the previous line then this is not flowed - if (resultLineQuoteDepth != actualQuoteDepth) resultLineFlowed = false; + if (resultLineQuoteDepth != actualQuoteDepth) { + resultLineFlowed = false; + } line = line.substring(actualQuoteDepth); } else { - // id quote-depth changes wrt the first line then this is not flowed - if (resultLineQuoteDepth > 0) resultLineFlowed = false; + // if quote-depth changes wrt the first line then this is not flowed + if (resultLineQuoteDepth > 0) { + resultLineFlowed = false; + } } - if (line.length() > 0 && line.charAt(0) == RFC2646_SPACE) + if (line.length() > 0 && line.charAt(0) == RFC2646_SPACE) { // Line space-stuffed line = line.substring(1); + } // if the previous was the last then it was not flowed - } else if (line == null) resultLineFlowed = false; + } else if (line == null) { + resultLineFlowed = false; + } // Add the PREVIOUS line. // This often will find the flow looking for a space as the last char of the line. - // With quote changes or signatures it could be the followinf line to void the flow. + // With quote changes or signatures it could be the following line to void the flow. if (!resultLineFlowed && i > 0) { - if (resultLineQuoteDepth > 0) resultLine.insert(0, RFC2646_SPACE); - for (int j = 0; j < resultLineQuoteDepth; j++) resultLine.insert(0, RFC2646_QUOTE); - if (result == null) result = new StringBuffer(); - else result.append(RFC2646_CRLF); + if (resultLineQuoteDepth > 0) { + resultLine.insert(0, RFC2646_SPACE); + } + for (int j = 0; j < resultLineQuoteDepth; j++) { + resultLine.insert(0, RFC2646_QUOTE); + } + if (result == null) { + result = new StringBuffer(); + } else { + result.append(RFC2646_CRLF); + } result.append(resultLine.toString()); resultLine = new StringBuffer(); resultLineFlowed = false; @@ -86,13 +101,16 @@ public final class FlowedMessageUtils { if (line != null) { if (!line.equals(RFC2646_SIGNATURE) && line.endsWith("" + RFC2646_SPACE) && i < lines.length - 1) { - // Line flowed (NOTE: for the split operation the line having i == lines.length is the last that does not end with RFC2646_CRLF) - if (delSp) line = line.substring(0, line.length() - 1); + // Line flowed (NOTE: for the split operation the line having i == lines.length is the last that + // does not end with RFC2646_CRLF) + if (delSp) { + line = line.substring(0, line.length() - 1); + } resultLineFlowed = true; + } else { + resultLineFlowed = false; } - else resultLineFlowed = false; - resultLine.append(line); } } -- GitLab From 2f78315cb4e5543da3caf963a7426371e7f993f5 Mon Sep 17 00:00:00 2001 From: cketti Date: Wed, 20 Apr 2022 22:44:01 +0200 Subject: [PATCH 25/75] Fix bug in `FlowedMessageUtils.deflow()` Properly handle the case when a quoted flowed line is followed by an empty line. --- .../java/com/fsck/k9/mail/internet/FlowedMessageUtils.java | 6 +++--- .../com/fsck/k9/mail/internet/FlowedMessageUtilsTest.kt | 2 -- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/mail/common/src/main/java/com/fsck/k9/mail/internet/FlowedMessageUtils.java b/mail/common/src/main/java/com/fsck/k9/mail/internet/FlowedMessageUtils.java index 870665b437..580a5acaa2 100644 --- a/mail/common/src/main/java/com/fsck/k9/mail/internet/FlowedMessageUtils.java +++ b/mail/common/src/main/java/com/fsck/k9/mail/internet/FlowedMessageUtils.java @@ -45,11 +45,11 @@ public final class FlowedMessageUtils { String line = i < lines.length ? lines[i] : null; int actualQuoteDepth = 0; - if (line != null && line.length() > 0) { + if (line != null) { if (line.equals(RFC2646_SIGNATURE)) { // signature handling (the previous line is not flowed) resultLineFlowed = false; - } else if (line.charAt(0) == RFC2646_QUOTE) { + } else if (line.length() > 0 && line.charAt(0) == RFC2646_QUOTE) { // Quote actualQuoteDepth = 1; while (actualQuoteDepth < line.length() && line.charAt(actualQuoteDepth) == RFC2646_QUOTE) { @@ -74,7 +74,7 @@ public final class FlowedMessageUtils { } // if the previous was the last then it was not flowed - } else if (line == null) { + } else { resultLineFlowed = false; } diff --git a/mail/common/src/test/java/com/fsck/k9/mail/internet/FlowedMessageUtilsTest.kt b/mail/common/src/test/java/com/fsck/k9/mail/internet/FlowedMessageUtilsTest.kt index 5aecb3ad99..cf70f7145a 100644 --- a/mail/common/src/test/java/com/fsck/k9/mail/internet/FlowedMessageUtilsTest.kt +++ b/mail/common/src/test/java/com/fsck/k9/mail/internet/FlowedMessageUtilsTest.kt @@ -2,7 +2,6 @@ package com.fsck.k9.mail.internet import com.fsck.k9.mail.crlf import com.google.common.truth.Truth.assertThat -import org.junit.Ignore import org.junit.Test private const val DEL_SP_NO = false @@ -98,7 +97,6 @@ class FlowedMessageUtilsTest { ) } - @Ignore("Fails because of a bug in the code. See GH-6029") @Test fun `deflow() with quoted text ending in space followed by empty line`() { val input = "> Quoted \r\n" + -- GitLab From f7b6b8371f0b20c3ebc754da02daeee3ddcd43be Mon Sep 17 00:00:00 2001 From: cketti Date: Thu, 21 Apr 2022 23:40:27 +0200 Subject: [PATCH 26/75] Rename .java to .kt --- .../internet/{FlowedMessageUtils.java => FlowedMessageUtils.kt} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename mail/common/src/main/java/com/fsck/k9/mail/internet/{FlowedMessageUtils.java => FlowedMessageUtils.kt} (100%) diff --git a/mail/common/src/main/java/com/fsck/k9/mail/internet/FlowedMessageUtils.java b/mail/common/src/main/java/com/fsck/k9/mail/internet/FlowedMessageUtils.kt similarity index 100% rename from mail/common/src/main/java/com/fsck/k9/mail/internet/FlowedMessageUtils.java rename to mail/common/src/main/java/com/fsck/k9/mail/internet/FlowedMessageUtils.kt -- GitLab From ef8d9abed393418c2549430a9bad376bb1c9b6b2 Mon Sep 17 00:00:00 2001 From: cketti Date: Thu, 21 Apr 2022 23:40:27 +0200 Subject: [PATCH 27/75] Rewrite `FlowedMessageUtils.deflow()` This new version should use a lot less allocations. --- .../k9/mail/internet/FlowedMessageUtils.kt | 161 +++++++----------- .../mail/internet/FlowedMessageUtilsTest.kt | 48 ++++-- 2 files changed, 93 insertions(+), 116 deletions(-) diff --git a/mail/common/src/main/java/com/fsck/k9/mail/internet/FlowedMessageUtils.kt b/mail/common/src/main/java/com/fsck/k9/mail/internet/FlowedMessageUtils.kt index 580a5acaa2..600fe234f3 100644 --- a/mail/common/src/main/java/com/fsck/k9/mail/internet/FlowedMessageUtils.kt +++ b/mail/common/src/main/java/com/fsck/k9/mail/internet/FlowedMessageUtils.kt @@ -1,120 +1,83 @@ -package com.fsck.k9.mail.internet; - +package com.fsck.k9.mail.internet /** - * Adapted from the Apache James project, see - * https://james.apache.org/mailet/base/apidocs/org/apache/mailet/base/FlowedMessageUtils.html - * - *

Manages texts encoded as text/plain; format=flowed.

- *

As a reference see:

- * - *

Note

- *
    - *
  • In order to decode, the input text must belong to a mail with headers similar to: - * Content-Type: text/plain; charset="CHARSET"; [delsp="yes|no"; ]format="flowed" - * (the quotes around CHARSET are not mandatory). - * Furthermore the header Content-Transfer-Encoding MUST NOT BE Quoted-Printable - * (see RFC3676 paragraph 4.2).(In fact this happens often for non 7bit messages). - *
  • - *
+ * Decodes text encoded as `text/plain; format=flowed` (RFC 3676). */ -public final class FlowedMessageUtils { - private static final char RFC2646_SPACE = ' '; - private static final char RFC2646_QUOTE = '>'; - private static final String RFC2646_SIGNATURE = "-- "; - private static final String RFC2646_CRLF = "\r\n"; - - private FlowedMessageUtils() { - // this class cannot be instantiated - } +object FlowedMessageUtils { + private const val QUOTE = '>' + private const val SPACE = ' ' + private const val CR = '\r' + private const val LF = '\n' + private const val SIGNATURE = "-- " + private const val CRLF = "\r\n" - /** - * Decodes a text previously wrapped using "format=flowed". - */ - public static String deflow(String text, boolean delSp) { - String[] lines = text.split("\r\n|\n", -1); - StringBuffer result = null; - StringBuffer resultLine = new StringBuffer(); - int resultLineQuoteDepth = 0; - boolean resultLineFlowed = false; - // One more cycle, to close the last line - for (int i = 0; i <= lines.length; i++) { - String line = i < lines.length ? lines[i] : null; - int actualQuoteDepth = 0; - - if (line != null) { - if (line.equals(RFC2646_SIGNATURE)) { - // signature handling (the previous line is not flowed) - resultLineFlowed = false; - } else if (line.length() > 0 && line.charAt(0) == RFC2646_QUOTE) { - // Quote - actualQuoteDepth = 1; - while (actualQuoteDepth < line.length() && line.charAt(actualQuoteDepth) == RFC2646_QUOTE) { - actualQuoteDepth++; - } - // if quote-depth changes wrt the previous line then this is not flowed - if (resultLineQuoteDepth != actualQuoteDepth) { - resultLineFlowed = false; - } - line = line.substring(actualQuoteDepth); + @JvmStatic + fun deflow(text: String, delSp: Boolean): String { + var lineStartIndex = 0 + var lastLineQuoteDepth = 0 + var lastLineFlowed = false - } else { - // if quote-depth changes wrt the first line then this is not flowed - if (resultLineQuoteDepth > 0) { - resultLineFlowed = false; - } + return buildString { + while (lineStartIndex <= text.lastIndex) { + var quoteDepth = 0 + while (lineStartIndex <= text.lastIndex && text[lineStartIndex] == QUOTE) { + quoteDepth++ + lineStartIndex++ } - if (line.length() > 0 && line.charAt(0) == RFC2646_SPACE) { - // Line space-stuffed - line = line.substring(1); + // Remove space stuffing + if (lineStartIndex <= text.lastIndex && text[lineStartIndex] == SPACE) { + lineStartIndex++ } - // if the previous was the last then it was not flowed - } else { - resultLineFlowed = false; - } + // We support both LF and CRLF line endings. To cover both cases we search for LF. + val lineFeedIndex = text.indexOf(LF, lineStartIndex) + val lineBreakFound = lineFeedIndex != -1 - // Add the PREVIOUS line. - // This often will find the flow looking for a space as the last char of the line. - // With quote changes or signatures it could be the following line to void the flow. - if (!resultLineFlowed && i > 0) { - if (resultLineQuoteDepth > 0) { - resultLine.insert(0, RFC2646_SPACE); + var lineEndIndex = if (lineBreakFound) lineFeedIndex else text.length + if (lineEndIndex > 0 && text[lineEndIndex - 1] == CR) { + lineEndIndex-- } - for (int j = 0; j < resultLineQuoteDepth; j++) { - resultLine.insert(0, RFC2646_QUOTE); - } - if (result == null) { - result = new StringBuffer(); - } else { - result.append(RFC2646_CRLF); + + if (lastLineFlowed && quoteDepth != lastLineQuoteDepth) { + append(CRLF) + lastLineFlowed = false } - result.append(resultLine.toString()); - resultLine = new StringBuffer(); - resultLineFlowed = false; - } - resultLineQuoteDepth = actualQuoteDepth; - if (line != null) { - if (!line.equals(RFC2646_SIGNATURE) && line.endsWith("" + RFC2646_SPACE) && i < lines.length - 1) { - // Line flowed (NOTE: for the split operation the line having i == lines.length is the last that - // does not end with RFC2646_CRLF) + val lineIsSignatureMarker = lineEndIndex - lineStartIndex == SIGNATURE.length && + text.regionMatches(lineStartIndex, SIGNATURE, 0, SIGNATURE.length) + + var lineFlowed = false + if (lineIsSignatureMarker) { + if (lastLineFlowed) { + append(CRLF) + lastLineFlowed = false + } + } else if (lineEndIndex > lineStartIndex && text[lineEndIndex - 1] == SPACE) { + lineFlowed = true if (delSp) { - line = line.substring(0, line.length() - 1); + lineEndIndex-- + } + } + + if (!lastLineFlowed && quoteDepth > 0) { + // This is not a continuation line, so prefix the text with quote characters. + repeat(quoteDepth) { + append(QUOTE) } - resultLineFlowed = true; - } else { - resultLineFlowed = false; + append(SPACE) } - resultLine.append(line); + append(text, lineStartIndex, lineEndIndex) + + if (!lineFlowed && lineBreakFound) { + append(CRLF) + } + + lineStartIndex = if (lineBreakFound) lineFeedIndex + 1 else text.length + lastLineQuoteDepth = quoteDepth + lastLineFlowed = lineFlowed } } - - return result.toString(); } } diff --git a/mail/common/src/test/java/com/fsck/k9/mail/internet/FlowedMessageUtilsTest.kt b/mail/common/src/test/java/com/fsck/k9/mail/internet/FlowedMessageUtilsTest.kt index cf70f7145a..f9ef77ede7 100644 --- a/mail/common/src/test/java/com/fsck/k9/mail/internet/FlowedMessageUtilsTest.kt +++ b/mail/common/src/test/java/com/fsck/k9/mail/internet/FlowedMessageUtilsTest.kt @@ -4,16 +4,13 @@ import com.fsck.k9.mail.crlf import com.google.common.truth.Truth.assertThat import org.junit.Test -private const val DEL_SP_NO = false -private const val DEL_SP_YES = true - class FlowedMessageUtilsTest { @Test fun `deflow() with simple text`() { val input = "Text that should be \r\n" + "displayed on one line" - val result = FlowedMessageUtils.deflow(input, DEL_SP_NO) + val result = FlowedMessageUtils.deflow(input, delSp = false) assertThat(result).isEqualTo("Text that should be displayed on one line") } @@ -27,7 +24,7 @@ class FlowedMessageUtilsTest { "Text that should retain\r\n" + "its line break." - val result = FlowedMessageUtils.deflow(input, DEL_SP_NO) + val result = FlowedMessageUtils.deflow(input, delSp = false) assertThat(result).isEqualTo( """ @@ -42,7 +39,7 @@ class FlowedMessageUtilsTest { fun `deflow() with nothing to do`() { val input = "Line one\r\nLine two\r\n" - val result = FlowedMessageUtils.deflow(input, DEL_SP_NO) + val result = FlowedMessageUtils.deflow(input, delSp = false) assertThat(result).isEqualTo(input) } @@ -56,7 +53,7 @@ class FlowedMessageUtilsTest { "Some more text that should be \r\n" + "displayed on one line.\r\n" - val result = FlowedMessageUtils.deflow(input, DEL_SP_NO) + val result = FlowedMessageUtils.deflow(input, delSp = false) assertThat(result).isEqualTo( """ @@ -74,7 +71,7 @@ class FlowedMessageUtilsTest { val input = "> Quoted text \r\n" + "Some other text" - val result = FlowedMessageUtils.deflow(input, DEL_SP_NO) + val result = FlowedMessageUtils.deflow(input, delSp = false) assertThat(result).isEqualTo("> Quoted text \r\nSome other text") } @@ -86,7 +83,7 @@ class FlowedMessageUtilsTest { "> is here\r\n" + "Some other text" - val result = FlowedMessageUtils.deflow(input, DEL_SP_NO) + val result = FlowedMessageUtils.deflow(input, delSp = false) assertThat(result).isEqualTo( """ @@ -103,7 +100,7 @@ class FlowedMessageUtilsTest { "\r\n" + "Text" - val result = FlowedMessageUtils.deflow(input, DEL_SP_NO) + val result = FlowedMessageUtils.deflow(input, delSp = false) assertThat(result).isEqualTo(input) } @@ -112,7 +109,7 @@ class FlowedMessageUtilsTest { fun `deflow() with delSp=true`() { val input = "Text that is wrapped mid wo \r\nrd" - val result = FlowedMessageUtils.deflow(input, DEL_SP_YES) + val result = FlowedMessageUtils.deflow(input, delSp = true) assertThat(result).isEqualTo("Text that is wrapped mid word") } @@ -122,7 +119,7 @@ class FlowedMessageUtilsTest { val input = "> Quoted te \r\n" + "> xt" - val result = FlowedMessageUtils.deflow(input, DEL_SP_YES) + val result = FlowedMessageUtils.deflow(input, delSp = true) assertThat(result).isEqualTo("> Quoted text") } @@ -132,7 +129,7 @@ class FlowedMessageUtilsTest { val input = "Text that should be \r\n" + " displayed on one line" - val result = FlowedMessageUtils.deflow(input, DEL_SP_NO) + val result = FlowedMessageUtils.deflow(input, delSp = false) assertThat(result).isEqualTo("Text that should be displayed on one line") } @@ -143,7 +140,7 @@ class FlowedMessageUtilsTest { " Line 2\r\n" + " Line 3\r\n" - val result = FlowedMessageUtils.deflow(input, DEL_SP_NO) + val result = FlowedMessageUtils.deflow(input, delSp = false) assertThat(result).isEqualTo("Line 1\r\nLine 2\r\nLine 3\r\n") } @@ -153,7 +150,7 @@ class FlowedMessageUtilsTest { val input = "> Text that should be \r\n" + "> displayed on one line" - val result = FlowedMessageUtils.deflow(input, DEL_SP_NO) + val result = FlowedMessageUtils.deflow(input, delSp = false) assertThat(result).isEqualTo("> Text that should be displayed on one line") } @@ -167,7 +164,7 @@ class FlowedMessageUtilsTest { "Signature \r\n" + "text" - val result = FlowedMessageUtils.deflow(input, DEL_SP_NO) + val result = FlowedMessageUtils.deflow(input, delSp = false) assertThat(result).isEqualTo( """ @@ -188,7 +185,7 @@ class FlowedMessageUtilsTest { "> Signature \r\n" + "> text" - val result = FlowedMessageUtils.deflow(input, DEL_SP_NO) + val result = FlowedMessageUtils.deflow(input, delSp = false) assertThat(result).isEqualTo( """ @@ -199,4 +196,21 @@ class FlowedMessageUtilsTest { """.trimIndent().crlf() ) } + + @Test + fun `deflow() with flowed line followed by signature separator`() { + val input = "Fake flowed line \r\n" + + "-- \r\n" + + "Signature" + + val result = FlowedMessageUtils.deflow(input, delSp = true) + + assertThat(result).isEqualTo( + """ + Fake flowed line + --${" "} + Signature + """.trimIndent().crlf() + ) + } } -- GitLab From f69a6dc2b9fcd9f4a5f3a08eb714f4b410d07c87 Mon Sep 17 00:00:00 2001 From: cketti Date: Mon, 25 Apr 2022 17:40:19 +0200 Subject: [PATCH 28/75] Update links to user manual --- .github/CONTRIBUTING.md | 2 +- README.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index ef762dd540..9592db5454 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -3,7 +3,7 @@ If the app is not behaving like it should, it's not necessarily a bug. Please consult the following resources before submitting a new issue. -* [User Manual](https://k9mail.app/documentation/) +* [User Manual](https://docs.k9mail.app/) * [Frequently Asked Questions](https://forum.k9mail.app/c/faq) * [Support Forum](https://forum.k9mail.app/) diff --git a/README.md b/README.md index c896cdd8c0..90f11216bb 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ in each version of K-9 Mail. If the app is not behaving like it should, you might find these resources helpful: -- [User Manual](https://k9mail.app/documentation/) +- [User Manual](https://docs.k9mail.app/) - [Frequently Asked Questions](https://forum.k9mail.app/c/faq) - [Support Forum](https://forum.k9mail.app/) -- GitLab From 53ea4fcc2d7777b41d4bf5cee2a705ebab82b407 Mon Sep 17 00:00:00 2001 From: cketti Date: Mon, 25 Apr 2022 23:35:48 +0200 Subject: [PATCH 29/75] Silently ignore the APG crypto provider if found --- .../dialog/ApgDeprecationWarningDialog.java | 43 -------- .../account/OpenPgpAppSelectDialog.java | 57 ----------- .../main/res/drawable-hdpi/ic_apg_small.png | Bin 4533 -> 0 bytes .../main/res/drawable-mdpi/ic_apg_small.png | Bin 2572 -> 0 bytes .../main/res/drawable-xhdpi/ic_apg_small.png | Bin 7137 -> 0 bytes .../main/res/drawable-xxhdpi/ic_apg_small.png | Bin 13658 -> 0 bytes .../res/drawable-xxxhdpi/ic_apg_small.png | Bin 20847 -> 0 bytes .../main/res/layout/dialog_apg_deprecated.xml | 92 ------------------ app/ui/legacy/src/main/res/values/strings.xml | 8 -- 9 files changed, 200 deletions(-) delete mode 100644 app/ui/legacy/src/main/java/com/fsck/k9/ui/dialog/ApgDeprecationWarningDialog.java delete mode 100644 app/ui/legacy/src/main/res/drawable-hdpi/ic_apg_small.png delete mode 100644 app/ui/legacy/src/main/res/drawable-mdpi/ic_apg_small.png delete mode 100644 app/ui/legacy/src/main/res/drawable-xhdpi/ic_apg_small.png delete mode 100644 app/ui/legacy/src/main/res/drawable-xxhdpi/ic_apg_small.png delete mode 100644 app/ui/legacy/src/main/res/drawable-xxxhdpi/ic_apg_small.png delete mode 100644 app/ui/legacy/src/main/res/layout/dialog_apg_deprecated.xml diff --git a/app/ui/legacy/src/main/java/com/fsck/k9/ui/dialog/ApgDeprecationWarningDialog.java b/app/ui/legacy/src/main/java/com/fsck/k9/ui/dialog/ApgDeprecationWarningDialog.java deleted file mode 100644 index 093cff9207..0000000000 --- a/app/ui/legacy/src/main/java/com/fsck/k9/ui/dialog/ApgDeprecationWarningDialog.java +++ /dev/null @@ -1,43 +0,0 @@ -package com.fsck.k9.ui.dialog; - - -import android.annotation.SuppressLint; -import android.app.AlertDialog; -import android.app.Dialog; -import android.content.Context; -import android.content.DialogInterface; -import android.text.method.LinkMovementMethod; -import android.view.LayoutInflater; -import android.view.View; -import android.widget.TextView; - -import com.fsck.k9.ui.R; - - -public class ApgDeprecationWarningDialog extends AlertDialog { - public ApgDeprecationWarningDialog(Context context) { - super(context); - - LayoutInflater inflater = LayoutInflater.from(context); - - @SuppressLint("InflateParams") - View contentView = inflater.inflate(R.layout.dialog_apg_deprecated, null); - - TextView textViewLearnMore = contentView.findViewById(R.id.apg_learn_more); - makeTextViewLinksClickable(textViewLearnMore); - - setIcon(R.drawable.ic_apg_small); - setTitle(R.string.apg_deprecated_title); - setView(contentView); - setButton(Dialog.BUTTON_POSITIVE, context.getString(R.string.apg_deprecated_ok), new OnClickListener() { - @Override - public void onClick(DialogInterface dialogInterface, int i) { - cancel(); - } - }); - } - - private void makeTextViewLinksClickable(TextView textView) { - textView.setMovementMethod(LinkMovementMethod.getInstance()); - } -} diff --git a/app/ui/legacy/src/main/java/com/fsck/k9/ui/settings/account/OpenPgpAppSelectDialog.java b/app/ui/legacy/src/main/java/com/fsck/k9/ui/settings/account/OpenPgpAppSelectDialog.java index ec01b6ece7..d9045bc4fa 100644 --- a/app/ui/legacy/src/main/java/com/fsck/k9/ui/settings/account/OpenPgpAppSelectDialog.java +++ b/app/ui/legacy/src/main/java/com/fsck/k9/ui/settings/account/OpenPgpAppSelectDialog.java @@ -30,7 +30,6 @@ import com.fsck.k9.Preferences; import com.fsck.k9.ui.R; import com.fsck.k9.ui.base.K9Activity; import com.fsck.k9.ui.base.ThemeType; -import com.fsck.k9.ui.dialog.ApgDeprecationWarningDialog; import org.openintents.openpgp.util.OpenPgpApi; import org.openintents.openpgp.util.OpenPgpProviderUtil; import timber.log.Timber; @@ -40,11 +39,8 @@ public class OpenPgpAppSelectDialog extends K9Activity { private static final String EXTRA_ACCOUNT = "account"; private static final String OPENKEYCHAIN_PACKAGE = "org.sufficientlysecure.keychain"; - private static final String PACKAGE_NAME_APG = "org.thialfihar.android.apg"; - private static final String APG_PROVIDER_PLACEHOLDER = "apg-placeholder"; public static final String FRAG_OPENPGP_SELECT = "openpgp_select"; - public static final String FRAG_APG_DEPRECATE = "apg_deprecate"; public static final String FRAG_OPENKEYCHAIN_INFO = "openkeychain_info"; private static final Intent MARKET_INTENT = new Intent(Intent.ACTION_VIEW, Uri.parse( @@ -53,7 +49,6 @@ public class OpenPgpAppSelectDialog extends K9Activity { String.format("https://play.google.com/store/apps/details?id=%s", OPENKEYCHAIN_PACKAGE))); - private boolean isStopped; private Account account; public static void startOpenPgpChooserActivity(Context context, Account account) { @@ -76,7 +71,6 @@ public class OpenPgpAppSelectDialog extends K9Activity { @Override protected void onStart() { - isStopped = false; super.onStart(); List openPgpProviderPackages = OpenPgpProviderUtil.getOpenPgpProviderPackages(this); @@ -93,7 +87,6 @@ public class OpenPgpAppSelectDialog extends K9Activity { @Override protected void onStop() { - isStopped = true; super.onStop(); } @@ -102,11 +95,6 @@ public class OpenPgpAppSelectDialog extends K9Activity { fragment.show(getSupportFragmentManager(), FRAG_OPENPGP_SELECT); } - private void showApgDeprecationDialogFragment() { - ApgDeprecationDialogFragment fragment = new ApgDeprecationDialogFragment(); - fragment.show(getSupportFragmentManager(), FRAG_APG_DEPRECATE); - } - private void showOpenKeychainInfoFragment() { OpenKeychainInfoFragment fragment = new OpenKeychainInfoFragment(); fragment.show(getSupportFragmentManager(), FRAG_OPENKEYCHAIN_INFO); @@ -126,12 +114,6 @@ public class OpenPgpAppSelectDialog extends K9Activity { getResources().getDrawable(R.drawable.ic_action_cancel_launchersize_light)); openPgpProviderList.add(noneEntry); - if (isApgInstalled(getActivity())) { - Drawable icon = getResources().getDrawable(R.drawable.ic_apg_small); - openPgpProviderList.add(new OpenPgpProviderEntry( - APG_PROVIDER_PLACEHOLDER, getString(R.string.apg), icon)); - } - // search for OpenPGP providers... Intent intent = new Intent(OpenPgpApi.SERVICE_INTENT_2); List resInfo = getActivity().getPackageManager().queryIntentServices(intent, 0); @@ -170,13 +152,6 @@ public class OpenPgpAppSelectDialog extends K9Activity { } } - private boolean isApgInstalled(Context context) { - Intent intent = new Intent("org.openintents.openpgp.IOpenPgpService"); - intent.setPackage(PACKAGE_NAME_APG); - List resInfo = context.getPackageManager().queryIntentServices(intent, 0); - return resInfo != null && !resInfo.isEmpty(); - } - @Override public void onStop() { super.onStop(); @@ -251,27 +226,6 @@ public class OpenPgpAppSelectDialog extends K9Activity { } } - public static class ApgDeprecationDialogFragment extends DialogFragment { - @Override - public Dialog onCreateDialog(Bundle savedInstanceState) { - return new ApgDeprecationWarningDialog(getActivity()); - } - - @Override - public void onStop() { - super.onStop(); - - dismiss(); - } - - @Override - public void onDismiss(DialogInterface dialog) { - super.onDismiss(dialog); - - ((OpenPgpAppSelectDialog) getActivity()).onDismissApgDialog(); - } - } - public static class OpenKeychainInfoFragment extends DialogFragment { @NonNull @Override @@ -323,11 +277,6 @@ public class OpenPgpAppSelectDialog extends K9Activity { } public void onSelectProvider(String selectedPackage) { - if (APG_PROVIDER_PLACEHOLDER.equals(selectedPackage)) { - showApgDeprecationDialogFragment(); - return; - } - persistOpenPgpProviderSetting(selectedPackage); finish(); } @@ -337,12 +286,6 @@ public class OpenPgpAppSelectDialog extends K9Activity { Preferences.getPreferences(getApplicationContext()).saveAccount(account); } - public void onDismissApgDialog() { - if (!isStopped) { - showOpenPgpSelectDialogFragment(); - } - } - private static class OpenPgpProviderEntry { private String packageName; private String simpleName; diff --git a/app/ui/legacy/src/main/res/drawable-hdpi/ic_apg_small.png b/app/ui/legacy/src/main/res/drawable-hdpi/ic_apg_small.png deleted file mode 100644 index 29b41d89d2b6c1e4f60460a992a871640c5e8fc8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4533 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 Xjf!T`Z%1)q00000NkvXXu0mjfQ(?4j diff --git a/app/ui/legacy/src/main/res/drawable-xxhdpi/ic_apg_small.png b/app/ui/legacy/src/main/res/drawable-xxhdpi/ic_apg_small.png deleted file mode 100644 index 68df5fb5806ff996276eb2c7076a8b4d6dfc8e03..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 13658 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 diff --git a/app/ui/legacy/src/main/res/drawable-xxxhdpi/ic_apg_small.png b/app/ui/legacy/src/main/res/drawable-xxxhdpi/ic_apg_small.png deleted file mode 100644 index de98c497b386f8724294bbe4f57657d79cf117fd..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 20847 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 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/ui/legacy/src/main/res/values/strings.xml b/app/ui/legacy/src/main/res/values/strings.xml index bdef9991f0..54d46ca269 100644 --- a/app/ui/legacy/src/main/res/values/strings.xml +++ b/app/ui/legacy/src/main/res/values/strings.xml @@ -1148,14 +1148,6 @@ Please submit bug reports, contribute new features and ask questions at All signatures will be displayed Encryption unavailable in sign-only mode! Unsigned Text - APG Deprecation Warning - APG is no longer maintained! - Because of this, support for APG has been removed from K-9 Mail. - Development stopped in early 2014 - Contains unfixed security issues - You can click here to learn more. - Got it! - APG This email is encrypted This email has been encrypted with OpenPGP.\nTo read it, you need to install and configure a compatible OpenPGP App. Go to Settings -- GitLab From d71e5b40ac925e5ad19e936248aca99ab3f61778 Mon Sep 17 00:00:00 2001 From: Bastian Wilhelm Date: Thu, 28 Apr 2022 14:22:03 +0200 Subject: [PATCH 30/75] Replace AndroidX annotations with the ones from JetBrains --- mail/common/build.gradle | 1 - mail/common/src/main/java/com/fsck/k9/mail/Address.java | 6 +++--- .../src/main/java/com/fsck/k9/mail/BoundaryGenerator.java | 2 +- mail/common/src/main/java/com/fsck/k9/mail/Message.java | 5 ++--- mail/common/src/main/java/com/fsck/k9/mail/Part.java | 4 ++-- .../java/com/fsck/k9/mail/internet/MessageExtractor.java | 7 +++---- .../main/java/com/fsck/k9/mail/internet/MimeBodyPart.java | 4 +--- .../main/java/com/fsck/k9/mail/internet/MimeMessage.java | 5 ++--- .../main/java/com/fsck/k9/mail/internet/MimeUtility.java | 7 +++---- .../src/main/java/com/fsck/k9/mail/internet/TextBody.java | 3 +-- mail/protocols/imap/build.gradle | 1 - .../java/com/fsck/k9/mail/store/imap/ListResponse.java | 2 +- .../java/com/fsck/k9/mail/store/imap/RealImapStore.java | 3 +-- mail/protocols/pop3/build.gradle | 1 - .../main/java/com/fsck/k9/mail/store/pop3/Pop3Store.java | 5 ++--- mail/protocols/smtp/build.gradle | 1 - mail/protocols/webdav/build.gradle | 1 - 17 files changed, 22 insertions(+), 36 deletions(-) diff --git a/mail/common/build.gradle b/mail/common/build.gradle index f445e3e273..5ce857634f 100644 --- a/mail/common/build.gradle +++ b/mail/common/build.gradle @@ -10,7 +10,6 @@ dependencies { implementation "org.apache.james:apache-mime4j-dom:${versions.mime4j}" implementation "com.squareup.okio:okio:${versions.okio}" implementation "commons-io:commons-io:${versions.commonsIo}" - implementation "androidx.annotation:annotation:${versions.androidxAnnotation}" implementation "com.jakewharton.timber:timber:${versions.timber}" testImplementation project(":mail:testing") diff --git a/mail/common/src/main/java/com/fsck/k9/mail/Address.java b/mail/common/src/main/java/com/fsck/k9/mail/Address.java index d90aa220a3..587ce7a262 100644 --- a/mail/common/src/main/java/com/fsck/k9/mail/Address.java +++ b/mail/common/src/main/java/com/fsck/k9/mail/Address.java @@ -1,8 +1,6 @@ package com.fsck.k9.mail; -import androidx.annotation.NonNull; -import androidx.annotation.VisibleForTesting; import java.io.Serializable; import java.util.ArrayList; @@ -15,6 +13,8 @@ import org.apache.james.mime4j.codec.EncoderUtil; import org.apache.james.mime4j.dom.address.Mailbox; import org.apache.james.mime4j.dom.address.MailboxList; import org.apache.james.mime4j.field.address.DefaultAddressParser; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.VisibleForTesting; import timber.log.Timber; import android.text.TextUtils; @@ -29,7 +29,7 @@ public class Address implements Serializable { */ private static final Address[] EMPTY_ADDRESS_ARRAY = new Address[0]; - @NonNull + @NotNull private String mAddress; private String mPersonal; diff --git a/mail/common/src/main/java/com/fsck/k9/mail/BoundaryGenerator.java b/mail/common/src/main/java/com/fsck/k9/mail/BoundaryGenerator.java index 9b2452e610..87c4632732 100644 --- a/mail/common/src/main/java/com/fsck/k9/mail/BoundaryGenerator.java +++ b/mail/common/src/main/java/com/fsck/k9/mail/BoundaryGenerator.java @@ -3,7 +3,7 @@ package com.fsck.k9.mail; import java.util.Random; -import androidx.annotation.VisibleForTesting; +import org.jetbrains.annotations.VisibleForTesting; public class BoundaryGenerator { diff --git a/mail/common/src/main/java/com/fsck/k9/mail/Message.java b/mail/common/src/main/java/com/fsck/k9/mail/Message.java index c7aa729fdb..91e3a8043c 100644 --- a/mail/common/src/main/java/com/fsck/k9/mail/Message.java +++ b/mail/common/src/main/java/com/fsck/k9/mail/Message.java @@ -8,10 +8,9 @@ import java.util.EnumSet; import java.util.List; import java.util.Set; -import androidx.annotation.NonNull; - import com.fsck.k9.mail.filter.CountingOutputStream; import com.fsck.k9.mail.filter.EOLConvertingOutputStream; +import org.jetbrains.annotations.NotNull; import timber.log.Timber; @@ -98,7 +97,7 @@ public abstract class Message implements Part, Body { @Override public abstract void setHeader(String name, String value); - @NonNull + @NotNull @Override public abstract String[] getHeader(String name); diff --git a/mail/common/src/main/java/com/fsck/k9/mail/Part.java b/mail/common/src/main/java/com/fsck/k9/mail/Part.java index c3fb55e7c3..655c3bb5c9 100644 --- a/mail/common/src/main/java/com/fsck/k9/mail/Part.java +++ b/mail/common/src/main/java/com/fsck/k9/mail/Part.java @@ -4,7 +4,7 @@ package com.fsck.k9.mail; import java.io.IOException; import java.io.OutputStream; -import androidx.annotation.NonNull; +import org.jetbrains.annotations.NotNull; public interface Part { @@ -27,7 +27,7 @@ public interface Part { /** * Returns an array of headers of the given name. The array may be empty. */ - @NonNull + @NotNull String[] getHeader(String name); boolean isMimeType(String mimeType); diff --git a/mail/common/src/main/java/com/fsck/k9/mail/internet/MessageExtractor.java b/mail/common/src/main/java/com/fsck/k9/mail/internet/MessageExtractor.java index c54f469be8..c66d73c2c1 100644 --- a/mail/common/src/main/java/com/fsck/k9/mail/internet/MessageExtractor.java +++ b/mail/common/src/main/java/com/fsck/k9/mail/internet/MessageExtractor.java @@ -10,9 +10,6 @@ import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - import com.fsck.k9.mail.Body; import com.fsck.k9.mail.BodyPart; import com.fsck.k9.mail.Message; @@ -20,6 +17,8 @@ import com.fsck.k9.mail.MessagingException; import com.fsck.k9.mail.Multipart; import com.fsck.k9.mail.Part; import org.apache.commons.io.input.BoundedInputStream; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import timber.log.Timber; import static com.fsck.k9.mail.internet.CharsetSupport.fixupCharset; @@ -407,7 +406,7 @@ public class MessageExtractor { * A list that will receive the parts that are considered attachments. */ private static void findAttachments(Multipart multipart, Set knownTextParts, - @NonNull List attachments) { + @NotNull List attachments) { for (Part part : multipart.getBodyParts()) { Body body = part.getBody(); if (body instanceof Multipart) { diff --git a/mail/common/src/main/java/com/fsck/k9/mail/internet/MimeBodyPart.java b/mail/common/src/main/java/com/fsck/k9/mail/internet/MimeBodyPart.java index 1c86dec2d6..591f8230db 100644 --- a/mail/common/src/main/java/com/fsck/k9/mail/internet/MimeBodyPart.java +++ b/mail/common/src/main/java/com/fsck/k9/mail/internet/MimeBodyPart.java @@ -12,8 +12,6 @@ import java.io.IOException; import java.io.OutputStream; import java.io.OutputStreamWriter; -import androidx.annotation.NonNull; - import org.jetbrains.annotations.NotNull; import static com.fsck.k9.mail.internet.MimeUtility.isSameMimeType; @@ -86,7 +84,7 @@ public class MimeBodyPart extends BodyPart { mHeader.setHeader(name, value); } - @NonNull + @NotNull @Override public String[] getHeader(String name) { return mHeader.getHeader(name); diff --git a/mail/common/src/main/java/com/fsck/k9/mail/internet/MimeMessage.java b/mail/common/src/main/java/com/fsck/k9/mail/internet/MimeMessage.java index 81d8236949..6ec9b65dbf 100644 --- a/mail/common/src/main/java/com/fsck/k9/mail/internet/MimeMessage.java +++ b/mail/common/src/main/java/com/fsck/k9/mail/internet/MimeMessage.java @@ -15,8 +15,6 @@ import java.util.List; import java.util.Locale; import java.util.TimeZone; -import androidx.annotation.NonNull; - import com.fsck.k9.mail.Address; import com.fsck.k9.mail.Body; import com.fsck.k9.mail.BodyFactory; @@ -38,6 +36,7 @@ import org.apache.james.mime4j.parser.MimeStreamParser; import org.apache.james.mime4j.stream.BodyDescriptor; import org.apache.james.mime4j.stream.Field; import org.apache.james.mime4j.stream.MimeConfig; +import org.jetbrains.annotations.NotNull; import timber.log.Timber; @@ -434,7 +433,7 @@ public class MimeMessage extends Message { mHeader.setHeader(name, value); } - @NonNull + @NotNull @Override public String[] getHeader(String name) { return mHeader.getHeader(name); diff --git a/mail/common/src/main/java/com/fsck/k9/mail/internet/MimeUtility.java b/mail/common/src/main/java/com/fsck/k9/mail/internet/MimeUtility.java index c9b61ccd16..1991c33a54 100644 --- a/mail/common/src/main/java/com/fsck/k9/mail/internet/MimeUtility.java +++ b/mail/common/src/main/java/com/fsck/k9/mail/internet/MimeUtility.java @@ -9,9 +9,6 @@ import java.util.Locale; import java.util.Map; import java.util.regex.Pattern; -import androidx.annotation.NonNull; -import androidx.annotation.VisibleForTesting; - import com.fsck.k9.mail.Body; import com.fsck.k9.mail.BodyPart; import com.fsck.k9.mail.Message; @@ -21,6 +18,8 @@ import com.fsck.k9.mail.Part; import org.apache.james.mime4j.codec.Base64InputStream; import org.apache.james.mime4j.codec.QuotedPrintableInputStream; import org.apache.james.mime4j.util.MimeUtil; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.VisibleForTesting; import timber.log.Timber; @@ -1069,7 +1068,7 @@ public class MimeUtility { return DEFAULT_ATTACHMENT_MIME_TYPE; } - public static String getExtensionByMimeType(@NonNull String mimeType) { + public static String getExtensionByMimeType(@NotNull String mimeType) { String lowerCaseMimeType = mimeType.toLowerCase(Locale.US); for (String[] contentTypeMapEntry : MIME_TYPE_BY_EXTENSION_MAP) { if (contentTypeMapEntry[1].equals(lowerCaseMimeType)) { diff --git a/mail/common/src/main/java/com/fsck/k9/mail/internet/TextBody.java b/mail/common/src/main/java/com/fsck/k9/mail/internet/TextBody.java index 414be33d22..6a5902a11b 100644 --- a/mail/common/src/main/java/com/fsck/k9/mail/internet/TextBody.java +++ b/mail/common/src/main/java/com/fsck/k9/mail/internet/TextBody.java @@ -7,8 +7,6 @@ import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; -import androidx.annotation.Nullable; - import com.fsck.k9.mail.Body; import com.fsck.k9.mail.MessagingException; import com.fsck.k9.mail.filter.CountingOutputStream; @@ -17,6 +15,7 @@ import com.fsck.k9.mail.filter.SignSafeOutputStream; import org.apache.james.mime4j.Charsets; import org.apache.james.mime4j.codec.QuotedPrintableOutputStream; import org.apache.james.mime4j.util.MimeUtil; +import org.jetbrains.annotations.Nullable; public class TextBody implements Body, SizeAware { diff --git a/mail/protocols/imap/build.gradle b/mail/protocols/imap/build.gradle index 54e9e80df0..494445041a 100644 --- a/mail/protocols/imap/build.gradle +++ b/mail/protocols/imap/build.gradle @@ -12,7 +12,6 @@ dependencies { implementation "com.beetstra.jutf7:jutf7:1.0.0" implementation "commons-io:commons-io:${versions.commonsIo}" implementation "com.jakewharton.timber:timber:${versions.timber}" - implementation "androidx.annotation:annotation:${versions.androidxAnnotation}" testImplementation project(":mail:testing") testImplementation "org.robolectric:robolectric:${versions.robolectric}" diff --git a/mail/protocols/imap/src/main/java/com/fsck/k9/mail/store/imap/ListResponse.java b/mail/protocols/imap/src/main/java/com/fsck/k9/mail/store/imap/ListResponse.java index 4ae0d9dea7..57e38f73e2 100644 --- a/mail/protocols/imap/src/main/java/com/fsck/k9/mail/store/imap/ListResponse.java +++ b/mail/protocols/imap/src/main/java/com/fsck/k9/mail/store/imap/ListResponse.java @@ -5,7 +5,7 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; -import androidx.annotation.Nullable; +import org.jetbrains.annotations.Nullable; import static com.fsck.k9.mail.store.imap.ImapResponseParser.equalsIgnoreCase; diff --git a/mail/protocols/imap/src/main/java/com/fsck/k9/mail/store/imap/RealImapStore.java b/mail/protocols/imap/src/main/java/com/fsck/k9/mail/store/imap/RealImapStore.java index 3011442f73..1f89e0ce9c 100644 --- a/mail/protocols/imap/src/main/java/com/fsck/k9/mail/store/imap/RealImapStore.java +++ b/mail/protocols/imap/src/main/java/com/fsck/k9/mail/store/imap/RealImapStore.java @@ -13,8 +13,6 @@ import java.util.List; import java.util.Map; import java.util.Set; -import androidx.annotation.Nullable; - import com.fsck.k9.mail.AuthType; import com.fsck.k9.mail.ConnectionSecurity; import com.fsck.k9.mail.Flag; @@ -24,6 +22,7 @@ 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; import timber.log.Timber; diff --git a/mail/protocols/pop3/build.gradle b/mail/protocols/pop3/build.gradle index f7afb0a6ae..a99cfe4f06 100644 --- a/mail/protocols/pop3/build.gradle +++ b/mail/protocols/pop3/build.gradle @@ -8,7 +8,6 @@ dependencies { api project(":mail:common") implementation "com.jakewharton.timber:timber:${versions.timber}" - implementation "androidx.annotation:annotation:${versions.androidxAnnotation}" testImplementation project(":mail:testing") testImplementation "org.robolectric:robolectric:${versions.robolectric}" diff --git a/mail/protocols/pop3/src/main/java/com/fsck/k9/mail/store/pop3/Pop3Store.java b/mail/protocols/pop3/src/main/java/com/fsck/k9/mail/store/pop3/Pop3Store.java index cba76ff046..4542caaedd 100644 --- a/mail/protocols/pop3/src/main/java/com/fsck/k9/mail/store/pop3/Pop3Store.java +++ b/mail/protocols/pop3/src/main/java/com/fsck/k9/mail/store/pop3/Pop3Store.java @@ -4,13 +4,12 @@ package com.fsck.k9.mail.store.pop3; import java.util.HashMap; import java.util.Map; -import androidx.annotation.NonNull; - import com.fsck.k9.mail.AuthType; import com.fsck.k9.mail.ConnectionSecurity; import com.fsck.k9.mail.MessagingException; import com.fsck.k9.mail.ServerSettings; import com.fsck.k9.mail.ssl.TrustedSocketFactory; +import org.jetbrains.annotations.NotNull; public class Pop3Store { @@ -36,7 +35,7 @@ public class Pop3Store { authType = serverSettings.authenticationType; } - @NonNull + @NotNull public Pop3Folder getFolder(String name) { Pop3Folder folder = mFolders.get(name); if (folder == null) { diff --git a/mail/protocols/smtp/build.gradle b/mail/protocols/smtp/build.gradle index 478cd591b2..8ec2ecb1d2 100644 --- a/mail/protocols/smtp/build.gradle +++ b/mail/protocols/smtp/build.gradle @@ -11,7 +11,6 @@ dependencies { implementation "commons-io:commons-io:${versions.commonsIo}" implementation "com.squareup.okio:okio:${versions.okio}" implementation "com.jakewharton.timber:timber:${versions.timber}" - implementation "androidx.annotation:annotation:${versions.androidxAnnotation}" testImplementation project(":mail:testing") testImplementation "org.robolectric:robolectric:${versions.robolectric}" diff --git a/mail/protocols/webdav/build.gradle b/mail/protocols/webdav/build.gradle index 59634c5a6d..a14c8083cb 100644 --- a/mail/protocols/webdav/build.gradle +++ b/mail/protocols/webdav/build.gradle @@ -9,7 +9,6 @@ dependencies { implementation "commons-io:commons-io:${versions.commonsIo}" implementation "com.jakewharton.timber:timber:${versions.timber}" - implementation "androidx.annotation:annotation:${versions.androidxAnnotation}" testImplementation project(":mail:testing") testImplementation "org.robolectric:robolectric:${versions.robolectric}" -- GitLab From e2609f3a461f146f63e2910846a33d3fa3f12e7d Mon Sep 17 00:00:00 2001 From: cketti Date: Thu, 28 Apr 2022 18:56:33 +0200 Subject: [PATCH 31/75] Move `MailTo` and `ListHeaders` classes --- .../core/src/main/java/com/fsck/k9/helper}/ListHeaders.java | 3 +-- .../core}/src/main/java/com/fsck/k9/helper/MailTo.java | 0 app/core/src/main/java/com/fsck/k9/helper/ReplyToParser.java | 1 - .../src/test/java/com/fsck/k9/helper}/ListHeadersTest.java | 3 ++- .../core}/src/test/java/com/fsck/k9/helper/MailToTest.java | 0 .../src/test/java/com/fsck/k9/helper/ReplyToParserTest.java | 1 - 6 files changed, 3 insertions(+), 5 deletions(-) rename {mail/common/src/main/java/com/fsck/k9/mail/internet => app/core/src/main/java/com/fsck/k9/helper}/ListHeaders.java (96%) rename {mail/common => app/core}/src/main/java/com/fsck/k9/helper/MailTo.java (100%) rename {mail/common/src/test/java/com/fsck/k9/mail/internet => app/core/src/test/java/com/fsck/k9/helper}/ListHeadersTest.java (98%) rename {mail/common => app/core}/src/test/java/com/fsck/k9/helper/MailToTest.java (100%) diff --git a/mail/common/src/main/java/com/fsck/k9/mail/internet/ListHeaders.java b/app/core/src/main/java/com/fsck/k9/helper/ListHeaders.java similarity index 96% rename from mail/common/src/main/java/com/fsck/k9/mail/internet/ListHeaders.java rename to app/core/src/main/java/com/fsck/k9/helper/ListHeaders.java index 2bed0bafcc..52f1684607 100644 --- a/mail/common/src/main/java/com/fsck/k9/mail/internet/ListHeaders.java +++ b/app/core/src/main/java/com/fsck/k9/helper/ListHeaders.java @@ -1,4 +1,4 @@ -package com.fsck.k9.mail.internet; +package com.fsck.k9.helper; import android.net.Uri; @@ -8,7 +8,6 @@ import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; -import com.fsck.k9.helper.MailTo; import com.fsck.k9.mail.Address; import com.fsck.k9.mail.Message; diff --git a/mail/common/src/main/java/com/fsck/k9/helper/MailTo.java b/app/core/src/main/java/com/fsck/k9/helper/MailTo.java similarity index 100% rename from mail/common/src/main/java/com/fsck/k9/helper/MailTo.java rename to app/core/src/main/java/com/fsck/k9/helper/MailTo.java diff --git a/app/core/src/main/java/com/fsck/k9/helper/ReplyToParser.java b/app/core/src/main/java/com/fsck/k9/helper/ReplyToParser.java index 7cbb15e714..2d53a81392 100644 --- a/app/core/src/main/java/com/fsck/k9/helper/ReplyToParser.java +++ b/app/core/src/main/java/com/fsck/k9/helper/ReplyToParser.java @@ -12,7 +12,6 @@ import com.fsck.k9.Account; import com.fsck.k9.mail.Address; import com.fsck.k9.mail.Message; import com.fsck.k9.mail.Message.RecipientType; -import com.fsck.k9.mail.internet.ListHeaders; public class ReplyToParser { diff --git a/mail/common/src/test/java/com/fsck/k9/mail/internet/ListHeadersTest.java b/app/core/src/test/java/com/fsck/k9/helper/ListHeadersTest.java similarity index 98% rename from mail/common/src/test/java/com/fsck/k9/mail/internet/ListHeadersTest.java rename to app/core/src/test/java/com/fsck/k9/helper/ListHeadersTest.java index ba6bfbee31..b3d77e2ca9 100644 --- a/mail/common/src/test/java/com/fsck/k9/mail/internet/ListHeadersTest.java +++ b/app/core/src/test/java/com/fsck/k9/helper/ListHeadersTest.java @@ -1,9 +1,10 @@ -package com.fsck.k9.mail.internet; +package com.fsck.k9.helper; import com.fsck.k9.mail.Address; import com.fsck.k9.mail.K9LibRobolectricTestRunner; import com.fsck.k9.mail.Message; +import com.fsck.k9.mail.internet.MimeMessage; import org.junit.Test; import org.junit.runner.RunWith; diff --git a/mail/common/src/test/java/com/fsck/k9/helper/MailToTest.java b/app/core/src/test/java/com/fsck/k9/helper/MailToTest.java similarity index 100% rename from mail/common/src/test/java/com/fsck/k9/helper/MailToTest.java rename to app/core/src/test/java/com/fsck/k9/helper/MailToTest.java diff --git a/app/core/src/test/java/com/fsck/k9/helper/ReplyToParserTest.java b/app/core/src/test/java/com/fsck/k9/helper/ReplyToParserTest.java index 501b2731b9..6093ed15fd 100644 --- a/app/core/src/test/java/com/fsck/k9/helper/ReplyToParserTest.java +++ b/app/core/src/test/java/com/fsck/k9/helper/ReplyToParserTest.java @@ -10,7 +10,6 @@ import com.fsck.k9.helper.ReplyToParser.ReplyToAddresses; import com.fsck.k9.mail.Address; import com.fsck.k9.mail.Message; import com.fsck.k9.mail.Message.RecipientType; -import com.fsck.k9.mail.internet.ListHeaders; import org.junit.Before; import org.junit.Test; -- GitLab From 4ad2e04cff314fa91a4b4935270dd9fd9c9af42f Mon Sep 17 00:00:00 2001 From: cketti Date: Thu, 28 Apr 2022 22:02:48 +0200 Subject: [PATCH 32/75] Decouple `WebDavSocketFactory` from `DefaultTrustedSocketFactory` --- .../java/com/fsck/k9/backends/KoinModule.kt | 18 +++++++++++++++++- .../fsck/k9/backends/WebDavBackendFactory.kt | 6 ++++-- .../k9/mail/store/webdav/SniHostSetter.java | 10 ++++++++++ .../mail/store/webdav/WebDavSocketFactory.java | 8 +++++--- .../fsck/k9/mail/store/webdav/WebDavStore.java | 18 ++++++++++++------ .../k9/mail/transport/WebDavTransport.java | 7 ++++--- .../k9/mail/store/webdav/WebDavStoreTest.java | 8 ++++++-- 7 files changed, 58 insertions(+), 17 deletions(-) create mode 100644 mail/protocols/webdav/src/main/java/com/fsck/k9/mail/store/webdav/SniHostSetter.java diff --git a/app/k9mail/src/main/java/com/fsck/k9/backends/KoinModule.kt b/app/k9mail/src/main/java/com/fsck/k9/backends/KoinModule.kt index 8dd9438a3c..53a3a84895 100644 --- a/app/k9mail/src/main/java/com/fsck/k9/backends/KoinModule.kt +++ b/app/k9mail/src/main/java/com/fsck/k9/backends/KoinModule.kt @@ -5,7 +5,11 @@ import app.k9mail.dev.developmentModuleAdditions import com.fsck.k9.backend.BackendManager import com.fsck.k9.backend.imap.BackendIdleRefreshManager import com.fsck.k9.backend.imap.SystemAlarmManager +import com.fsck.k9.mail.ssl.DefaultTrustedSocketFactory import com.fsck.k9.mail.store.imap.IdleRefreshManager +import com.fsck.k9.mail.store.webdav.SniHostSetter +import javax.net.ssl.SSLSocket +import javax.net.ssl.SSLSocketFactory import org.koin.dsl.module val backendsModule = module { @@ -30,7 +34,19 @@ val backendsModule = module { single { AndroidAlarmManager(context = get(), alarmManager = get()) } single { BackendIdleRefreshManager(alarmManager = get()) } single { Pop3BackendFactory(get(), get()) } - single { WebDavBackendFactory(get(), get(), get()) } + single { + WebDavBackendFactory( + backendStorageFactory = get(), + trustManagerFactory = get(), + sniHostSetter = get(), + folderRepository = get() + ) + } + single { + SniHostSetter { factory, socket, hostname -> + DefaultTrustedSocketFactory.setSniHost(factory, socket, hostname) + } + } developmentModuleAdditions() } diff --git a/app/k9mail/src/main/java/com/fsck/k9/backends/WebDavBackendFactory.kt b/app/k9mail/src/main/java/com/fsck/k9/backends/WebDavBackendFactory.kt index 0098b96e46..0e4e588e33 100644 --- a/app/k9mail/src/main/java/com/fsck/k9/backends/WebDavBackendFactory.kt +++ b/app/k9mail/src/main/java/com/fsck/k9/backends/WebDavBackendFactory.kt @@ -6,6 +6,7 @@ import com.fsck.k9.backend.api.Backend import com.fsck.k9.backend.webdav.WebDavBackend import com.fsck.k9.mail.ssl.TrustManagerFactory import com.fsck.k9.mail.store.webdav.DraftsFolderProvider +import com.fsck.k9.mail.store.webdav.SniHostSetter import com.fsck.k9.mail.store.webdav.WebDavStore import com.fsck.k9.mail.transport.WebDavTransport import com.fsck.k9.mailstore.FolderRepository @@ -14,6 +15,7 @@ import com.fsck.k9.mailstore.K9BackendStorageFactory class WebDavBackendFactory( private val backendStorageFactory: K9BackendStorageFactory, private val trustManagerFactory: TrustManagerFactory, + private val sniHostSetter: SniHostSetter, private val folderRepository: FolderRepository ) : BackendFactory { override fun createBackend(account: Account): Backend { @@ -21,8 +23,8 @@ class WebDavBackendFactory( val backendStorage = backendStorageFactory.createBackendStorage(account) val serverSettings = account.incomingServerSettings val draftsFolderProvider = createDraftsFolderProvider(account) - val webDavStore = WebDavStore(trustManagerFactory, serverSettings, draftsFolderProvider) - val webDavTransport = WebDavTransport(trustManagerFactory, serverSettings, draftsFolderProvider) + val webDavStore = WebDavStore(trustManagerFactory, sniHostSetter, serverSettings, draftsFolderProvider) + val webDavTransport = WebDavTransport(trustManagerFactory, sniHostSetter, serverSettings, draftsFolderProvider) return WebDavBackend(accountName, backendStorage, webDavStore, webDavTransport) } diff --git a/mail/protocols/webdav/src/main/java/com/fsck/k9/mail/store/webdav/SniHostSetter.java b/mail/protocols/webdav/src/main/java/com/fsck/k9/mail/store/webdav/SniHostSetter.java new file mode 100644 index 0000000000..df323946ac --- /dev/null +++ b/mail/protocols/webdav/src/main/java/com/fsck/k9/mail/store/webdav/SniHostSetter.java @@ -0,0 +1,10 @@ +package com.fsck.k9.mail.store.webdav; + + +import javax.net.ssl.SSLSocket; +import javax.net.ssl.SSLSocketFactory; + + +public interface SniHostSetter { + void setSniHost(SSLSocketFactory factory, SSLSocket socket, String hostname); +} diff --git a/mail/protocols/webdav/src/main/java/com/fsck/k9/mail/store/webdav/WebDavSocketFactory.java b/mail/protocols/webdav/src/main/java/com/fsck/k9/mail/store/webdav/WebDavSocketFactory.java index 20ee771fd5..36d7abd8ff 100644 --- a/mail/protocols/webdav/src/main/java/com/fsck/k9/mail/store/webdav/WebDavSocketFactory.java +++ b/mail/protocols/webdav/src/main/java/com/fsck/k9/mail/store/webdav/WebDavSocketFactory.java @@ -7,7 +7,6 @@ import java.net.Socket; import java.security.KeyManagementException; import java.security.NoSuchAlgorithmException; -import com.fsck.k9.mail.ssl.DefaultTrustedSocketFactory; import com.fsck.k9.mail.ssl.TrustManagerFactory; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLSocket; @@ -22,10 +21,13 @@ import org.apache.http.params.HttpParams; * Using two socket factories looks suspicious. */ public class WebDavSocketFactory implements LayeredSocketFactory { + private final SniHostSetter sniHostSetter; private SSLSocketFactory mSocketFactory; private org.apache.http.conn.ssl.SSLSocketFactory mSchemeSocketFactory; - public WebDavSocketFactory(TrustManagerFactory trustManagerFactory, String host, int port) throws NoSuchAlgorithmException, KeyManagementException { + public WebDavSocketFactory(TrustManagerFactory trustManagerFactory, SniHostSetter sniHostSetter, + String host, int port) throws NoSuchAlgorithmException, KeyManagementException { + this.sniHostSetter = sniHostSetter; SSLContext sslContext = SSLContext.getInstance("TLS"); sslContext.init(null, new TrustManager[] { trustManagerFactory.getTrustManagerForDomain(host, port) @@ -62,7 +64,7 @@ public class WebDavSocketFactory implements LayeredSocketFactory { port, autoClose ); - DefaultTrustedSocketFactory.setSniHost(mSocketFactory, sslSocket, host); + sniHostSetter.setSniHost(mSocketFactory, sslSocket, host); //hostnameVerifier.verify(host, sslSocket); // verifyHostName() didn't blowup - good! return sslSocket; diff --git a/mail/protocols/webdav/src/main/java/com/fsck/k9/mail/store/webdav/WebDavStore.java b/mail/protocols/webdav/src/main/java/com/fsck/k9/mail/store/webdav/WebDavStore.java index bad815cca9..c11282ac4c 100644 --- a/mail/protocols/webdav/src/main/java/com/fsck/k9/mail/store/webdav/WebDavStore.java +++ b/mail/protocols/webdav/src/main/java/com/fsck/k9/mail/store/webdav/WebDavStore.java @@ -73,6 +73,7 @@ public class WebDavStore { private String mailboxPath; private final TrustManagerFactory trustManagerFactory; + private final SniHostSetter sniHostSetter; private final WebDavHttpClient.WebDavHttpClientFactory httpClientFactory; private WebDavHttpClient httpClient = null; private HttpContext httpContext = null; @@ -84,16 +85,19 @@ public class WebDavStore { private WebDavFolder sendFolder = null; private Map folderList = new HashMap<>(); - public WebDavStore(TrustManagerFactory trustManagerFactory, ServerSettings serverSettings, - DraftsFolderProvider draftsFolderProvider) { - this(trustManagerFactory, serverSettings, draftsFolderProvider, new WebDavHttpClient.WebDavHttpClientFactory()); + public WebDavStore(TrustManagerFactory trustManagerFactory, SniHostSetter sniHostSetter, + ServerSettings serverSettings, DraftsFolderProvider draftsFolderProvider) { + this(trustManagerFactory, sniHostSetter, serverSettings, draftsFolderProvider, + new WebDavHttpClient.WebDavHttpClientFactory()); } - public WebDavStore(TrustManagerFactory trustManagerFactory, ServerSettings serverSettings, - DraftsFolderProvider draftsFolderProvider, WebDavHttpClientFactory clientFactory) { + public WebDavStore(TrustManagerFactory trustManagerFactory, SniHostSetter sniHostSetter, + ServerSettings serverSettings, DraftsFolderProvider draftsFolderProvider, + WebDavHttpClientFactory clientFactory) { this.draftsFolderProvider = draftsFolderProvider; httpClientFactory = clientFactory; this.trustManagerFactory = trustManagerFactory; + this.sniHostSetter = sniHostSetter; hostname = serverSettings.host; port = serverSettings.port; @@ -765,7 +769,9 @@ public class WebDavStore { SchemeRegistry reg = httpClient.getConnectionManager().getSchemeRegistry(); try { - Scheme s = new Scheme("https", new WebDavSocketFactory(trustManagerFactory, hostname, 443), 443); + WebDavSocketFactory socketFactory = + new WebDavSocketFactory(trustManagerFactory, sniHostSetter, hostname, 443); + Scheme s = new Scheme("https", socketFactory, 443); reg.register(s); } catch (NoSuchAlgorithmException nsa) { Timber.e(nsa, "NoSuchAlgorithmException in getHttpClient"); diff --git a/mail/protocols/webdav/src/main/java/com/fsck/k9/mail/transport/WebDavTransport.java b/mail/protocols/webdav/src/main/java/com/fsck/k9/mail/transport/WebDavTransport.java index a9e8cac0ac..d133b0e740 100644 --- a/mail/protocols/webdav/src/main/java/com/fsck/k9/mail/transport/WebDavTransport.java +++ b/mail/protocols/webdav/src/main/java/com/fsck/k9/mail/transport/WebDavTransport.java @@ -10,15 +10,16 @@ import com.fsck.k9.mail.ServerSettings; import com.fsck.k9.mail.Transport; import com.fsck.k9.mail.ssl.TrustManagerFactory; import com.fsck.k9.mail.store.webdav.DraftsFolderProvider; +import com.fsck.k9.mail.store.webdav.SniHostSetter; import com.fsck.k9.mail.store.webdav.WebDavStore; import timber.log.Timber; public class WebDavTransport extends Transport { private WebDavStore store; - public WebDavTransport(TrustManagerFactory trustManagerFactory, ServerSettings serverSettings, - DraftsFolderProvider draftsFolderProvider) { - store = new WebDavStore(trustManagerFactory, serverSettings, draftsFolderProvider); + public WebDavTransport(TrustManagerFactory trustManagerFactory, SniHostSetter sniHostSetter, + ServerSettings serverSettings, DraftsFolderProvider draftsFolderProvider) { + store = new WebDavStore(trustManagerFactory, sniHostSetter, serverSettings, draftsFolderProvider); if (K9MailLib.isDebug()) Timber.d(">>> New WebDavTransport creation complete"); diff --git a/mail/protocols/webdav/src/test/java/com/fsck/k9/mail/store/webdav/WebDavStoreTest.java b/mail/protocols/webdav/src/test/java/com/fsck/k9/mail/store/webdav/WebDavStoreTest.java index 958ee1f159..8cb66466e2 100644 --- a/mail/protocols/webdav/src/test/java/com/fsck/k9/mail/store/webdav/WebDavStoreTest.java +++ b/mail/protocols/webdav/src/test/java/com/fsck/k9/mail/store/webdav/WebDavStoreTest.java @@ -70,6 +70,8 @@ public class WebDavStoreTest { @Mock private TrustManagerFactory trustManagerFactory; @Mock + private SniHostSetter sniHostSetter; + @Mock private DraftsFolderProvider draftsFolderProvider; private ArgumentCaptor requestCaptor; @@ -352,12 +354,14 @@ public class WebDavStoreTest { } private WebDavStore createWebDavStore() { - return new WebDavStore(trustManagerFactory, serverSettings, draftsFolderProvider, mockHttpClientFactory); + return new WebDavStore(trustManagerFactory, sniHostSetter, serverSettings, draftsFolderProvider, + mockHttpClientFactory); } private WebDavStore createWebDavStore(ConnectionSecurity connectionSecurity) { ServerSettings serverSettings = createServerSettings(connectionSecurity); - return new WebDavStore(trustManagerFactory, serverSettings, draftsFolderProvider, mockHttpClientFactory); + return new WebDavStore(trustManagerFactory, sniHostSetter, serverSettings, draftsFolderProvider, + mockHttpClientFactory); } private void configureHttpResponses(HttpResponse... responses) throws IOException { -- GitLab From 7f343e4fd1beb589a1fbc56249be6799edbdaac6 Mon Sep 17 00:00:00 2001 From: cketti Date: Thu, 28 Apr 2022 22:18:14 +0200 Subject: [PATCH 33/75] Move `DefaultTrustedSocketFactory` --- app/core/src/main/java/com/fsck/k9/KoinModule.kt | 2 +- .../java/com/fsck/k9/helper}/DefaultTrustedSocketFactory.java | 4 +++- .../src/main/java/com/fsck/k9/helper}/KeyChainKeyManager.java | 2 +- app/k9mail/src/main/java/com/fsck/k9/backends/KoinModule.kt | 4 +--- 4 files changed, 6 insertions(+), 6 deletions(-) rename {mail/common/src/main/java/com/fsck/k9/mail/ssl => app/core/src/main/java/com/fsck/k9/helper}/DefaultTrustedSocketFactory.java (97%) rename {mail/common/src/main/java/com/fsck/k9/mail/ssl => app/core/src/main/java/com/fsck/k9/helper}/KeyChainKeyManager.java (99%) diff --git a/app/core/src/main/java/com/fsck/k9/KoinModule.kt b/app/core/src/main/java/com/fsck/k9/KoinModule.kt index 05c30287cc..85cacac5b9 100644 --- a/app/core/src/main/java/com/fsck/k9/KoinModule.kt +++ b/app/core/src/main/java/com/fsck/k9/KoinModule.kt @@ -2,7 +2,7 @@ package com.fsck.k9 import android.content.Context import com.fsck.k9.helper.Contacts -import com.fsck.k9.mail.ssl.DefaultTrustedSocketFactory +import com.fsck.k9.helper.DefaultTrustedSocketFactory import com.fsck.k9.mail.ssl.LocalKeyStore import com.fsck.k9.mail.ssl.TrustManagerFactory import com.fsck.k9.mail.ssl.TrustedSocketFactory diff --git a/mail/common/src/main/java/com/fsck/k9/mail/ssl/DefaultTrustedSocketFactory.java b/app/core/src/main/java/com/fsck/k9/helper/DefaultTrustedSocketFactory.java similarity index 97% rename from mail/common/src/main/java/com/fsck/k9/mail/ssl/DefaultTrustedSocketFactory.java rename to app/core/src/main/java/com/fsck/k9/helper/DefaultTrustedSocketFactory.java index 4c9857e024..ce046daa9e 100644 --- a/mail/common/src/main/java/com/fsck/k9/mail/ssl/DefaultTrustedSocketFactory.java +++ b/app/core/src/main/java/com/fsck/k9/helper/DefaultTrustedSocketFactory.java @@ -1,4 +1,4 @@ -package com.fsck.k9.mail.ssl; +package com.fsck.k9.helper; import java.io.IOException; @@ -14,6 +14,8 @@ import android.net.SSLCertificateSocketFactory; import android.text.TextUtils; import com.fsck.k9.mail.MessagingException; +import com.fsck.k9.mail.ssl.TrustManagerFactory; +import com.fsck.k9.mail.ssl.TrustedSocketFactory; import javax.net.ssl.KeyManager; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLSocket; diff --git a/mail/common/src/main/java/com/fsck/k9/mail/ssl/KeyChainKeyManager.java b/app/core/src/main/java/com/fsck/k9/helper/KeyChainKeyManager.java similarity index 99% rename from mail/common/src/main/java/com/fsck/k9/mail/ssl/KeyChainKeyManager.java rename to app/core/src/main/java/com/fsck/k9/helper/KeyChainKeyManager.java index bfd90c196c..77e9fc4f83 100644 --- a/mail/common/src/main/java/com/fsck/k9/mail/ssl/KeyChainKeyManager.java +++ b/app/core/src/main/java/com/fsck/k9/helper/KeyChainKeyManager.java @@ -1,5 +1,5 @@ -package com.fsck.k9.mail.ssl; +package com.fsck.k9.helper; import java.net.Socket; import java.security.Principal; diff --git a/app/k9mail/src/main/java/com/fsck/k9/backends/KoinModule.kt b/app/k9mail/src/main/java/com/fsck/k9/backends/KoinModule.kt index 53a3a84895..5585a2bdcc 100644 --- a/app/k9mail/src/main/java/com/fsck/k9/backends/KoinModule.kt +++ b/app/k9mail/src/main/java/com/fsck/k9/backends/KoinModule.kt @@ -5,11 +5,9 @@ import app.k9mail.dev.developmentModuleAdditions import com.fsck.k9.backend.BackendManager import com.fsck.k9.backend.imap.BackendIdleRefreshManager import com.fsck.k9.backend.imap.SystemAlarmManager -import com.fsck.k9.mail.ssl.DefaultTrustedSocketFactory +import com.fsck.k9.helper.DefaultTrustedSocketFactory import com.fsck.k9.mail.store.imap.IdleRefreshManager import com.fsck.k9.mail.store.webdav.SniHostSetter -import javax.net.ssl.SSLSocket -import javax.net.ssl.SSLSocketFactory import org.koin.dsl.module val backendsModule = module { -- GitLab From 4b1dc23ebb1e517ca5d7f0cffcd4ee292eea156c Mon Sep 17 00:00:00 2001 From: cketti Date: Thu, 28 Apr 2022 22:39:39 +0200 Subject: [PATCH 34/75] Remove unused code from `OAuth2TokenProvider` --- .../k9/mail/oauth/OAuth2TokenProvider.java | 24 ------------------- .../store/imap/RealImapConnectionTest.java | 6 ----- 2 files changed, 30 deletions(-) diff --git a/mail/common/src/main/java/com/fsck/k9/mail/oauth/OAuth2TokenProvider.java b/mail/common/src/main/java/com/fsck/k9/mail/oauth/OAuth2TokenProvider.java index 5e0c27b7dc..db7abfb075 100644 --- a/mail/common/src/main/java/com/fsck/k9/mail/oauth/OAuth2TokenProvider.java +++ b/mail/common/src/main/java/com/fsck/k9/mail/oauth/OAuth2TokenProvider.java @@ -3,8 +3,6 @@ package com.fsck.k9.mail.oauth; import java.util.List; -import android.app.Activity; - import com.fsck.k9.mail.AuthenticationFailedException; @@ -20,18 +18,6 @@ public interface OAuth2TokenProvider { */ List getAccounts(); - /** - * Request API authorization. This is a foreground action that may produce a dialog to interact with. - * - * @param username - * Username - * @param activity - * The responsible activity - * @param callback - * A callback to process the asynchronous response - */ - void authorizeApi(String username, Activity activity, OAuth2TokenProviderAuthCallback callback); - /** * Fetch a token. No guarantees are provided for validity. */ @@ -47,14 +33,4 @@ public interface OAuth2TokenProvider { * Invalidating a token and then failure with a new token should be treated as a permanent failure. */ void invalidateToken(String username); - - - /** - * Provides an asynchronous response to an - * {@link OAuth2TokenProvider#authorizeApi(String, Activity, OAuth2TokenProviderAuthCallback)} request. - */ - interface OAuth2TokenProviderAuthCallback { - void success(); - void failure(AuthorizationException e); - } } diff --git a/mail/protocols/imap/src/test/java/com/fsck/k9/mail/store/imap/RealImapConnectionTest.java b/mail/protocols/imap/src/test/java/com/fsck/k9/mail/store/imap/RealImapConnectionTest.java index cbfd5f2a52..e707fce1ce 100644 --- a/mail/protocols/imap/src/test/java/com/fsck/k9/mail/store/imap/RealImapConnectionTest.java +++ b/mail/protocols/imap/src/test/java/com/fsck/k9/mail/store/imap/RealImapConnectionTest.java @@ -5,8 +5,6 @@ import java.io.IOException; import java.net.UnknownHostException; import java.util.List; -import android.app.Activity; - import com.fsck.k9.mail.AuthType; import com.fsck.k9.mail.AuthenticationFailedException; import com.fsck.k9.mail.CertificateValidationException; @@ -1070,10 +1068,6 @@ public class RealImapConnectionTest { throw new UnsupportedOperationException(); } - @Override - public void authorizeApi(String username, Activity activity, OAuth2TokenProviderAuthCallback callback) { - throw new UnsupportedOperationException(); - } }; } } -- GitLab From 6ea0bab0cf5481a4f2b170fdf3c633559bd50fe7 Mon Sep 17 00:00:00 2001 From: cketti Date: Thu, 28 Apr 2022 22:42:25 +0200 Subject: [PATCH 35/75] Remove `SuppressLint` annotations --- .../com/fsck/k9/mail/store/imap/mockserver/MockImapServer.java | 3 --- .../src/main/java/com/fsck/k9/mail/store/pop3/Pop3Folder.java | 3 --- .../test/java/com/fsck/k9/mail/store/pop3/MockPop3Server.java | 3 --- .../com/fsck/k9/mail/transport/mockServer/MockSmtpServer.java | 3 --- .../com/fsck/k9/mail/helpers/VeryTrustingTrustManager.java | 2 -- 5 files changed, 14 deletions(-) diff --git a/mail/protocols/imap/src/test/java/com/fsck/k9/mail/store/imap/mockserver/MockImapServer.java b/mail/protocols/imap/src/test/java/com/fsck/k9/mail/store/imap/mockserver/MockImapServer.java index c9cf9e5bba..140fa61df7 100644 --- a/mail/protocols/imap/src/test/java/com/fsck/k9/mail/store/imap/mockserver/MockImapServer.java +++ b/mail/protocols/imap/src/test/java/com/fsck/k9/mail/store/imap/mockserver/MockImapServer.java @@ -21,8 +21,6 @@ import java.util.concurrent.TimeUnit; import java.util.zip.Inflater; import java.util.zip.InflaterInputStream; -import android.annotation.SuppressLint; - import com.fsck.k9.mail.helpers.KeyStoreProvider; import com.jcraft.jzlib.JZlib; import com.jcraft.jzlib.ZOutputStream; @@ -36,7 +34,6 @@ import okio.Okio; import org.apache.commons.io.IOUtils; -@SuppressLint("NewApi") public class MockImapServer { private static final byte[] CRLF = { '\r', '\n' }; diff --git a/mail/protocols/pop3/src/main/java/com/fsck/k9/mail/store/pop3/Pop3Folder.java b/mail/protocols/pop3/src/main/java/com/fsck/k9/mail/store/pop3/Pop3Folder.java index a5c526e93a..870b9015aa 100644 --- a/mail/protocols/pop3/src/main/java/com/fsck/k9/mail/store/pop3/Pop3Folder.java +++ b/mail/protocols/pop3/src/main/java/com/fsck/k9/mail/store/pop3/Pop3Folder.java @@ -10,8 +10,6 @@ import java.util.Locale; import java.util.Map; import java.util.Set; -import android.annotation.SuppressLint; - import com.fsck.k9.mail.FetchProfile; import com.fsck.k9.mail.Flag; import com.fsck.k9.mail.K9MailLib; @@ -32,7 +30,6 @@ public class Pop3Folder { private Pop3Store pop3Store; private Map uidToMsgMap = new HashMap<>(); - @SuppressLint("UseSparseArrays") private Map msgNumToMsgMap = new HashMap<>(); private Map uidToMsgNumMap = new HashMap<>(); private String name; diff --git a/mail/protocols/pop3/src/test/java/com/fsck/k9/mail/store/pop3/MockPop3Server.java b/mail/protocols/pop3/src/test/java/com/fsck/k9/mail/store/pop3/MockPop3Server.java index 0742016b08..a5142829db 100644 --- a/mail/protocols/pop3/src/test/java/com/fsck/k9/mail/store/pop3/MockPop3Server.java +++ b/mail/protocols/pop3/src/test/java/com/fsck/k9/mail/store/pop3/MockPop3Server.java @@ -21,8 +21,6 @@ import java.util.concurrent.TimeUnit; import java.util.zip.Inflater; import java.util.zip.InflaterInputStream; -import android.annotation.SuppressLint; - import com.fsck.k9.mail.helpers.KeyStoreProvider; import com.jcraft.jzlib.JZlib; import com.jcraft.jzlib.ZOutputStream; @@ -36,7 +34,6 @@ import okio.Okio; import org.apache.commons.io.IOUtils; -@SuppressLint("NewApi") public class MockPop3Server { private static final byte[] CRLF = { '\r', '\n' }; diff --git a/mail/protocols/smtp/src/test/java/com/fsck/k9/mail/transport/mockServer/MockSmtpServer.java b/mail/protocols/smtp/src/test/java/com/fsck/k9/mail/transport/mockServer/MockSmtpServer.java index 0a88d21110..a4a994f9fb 100644 --- a/mail/protocols/smtp/src/test/java/com/fsck/k9/mail/transport/mockServer/MockSmtpServer.java +++ b/mail/protocols/smtp/src/test/java/com/fsck/k9/mail/transport/mockServer/MockSmtpServer.java @@ -21,8 +21,6 @@ import java.util.concurrent.TimeUnit; import java.util.zip.Inflater; import java.util.zip.InflaterInputStream; -import android.annotation.SuppressLint; - import com.fsck.k9.mail.helpers.KeyStoreProvider; import com.jcraft.jzlib.JZlib; import com.jcraft.jzlib.ZOutputStream; @@ -36,7 +34,6 @@ import okio.Okio; import org.apache.commons.io.IOUtils; -@SuppressLint("NewApi") public class MockSmtpServer { private static final byte[] CRLF = { '\r', '\n' }; diff --git a/mail/testing/src/main/java/com/fsck/k9/mail/helpers/VeryTrustingTrustManager.java b/mail/testing/src/main/java/com/fsck/k9/mail/helpers/VeryTrustingTrustManager.java index a455d55326..843358a90d 100644 --- a/mail/testing/src/main/java/com/fsck/k9/mail/helpers/VeryTrustingTrustManager.java +++ b/mail/testing/src/main/java/com/fsck/k9/mail/helpers/VeryTrustingTrustManager.java @@ -1,6 +1,5 @@ package com.fsck.k9.mail.helpers; -import android.annotation.SuppressLint; import java.security.cert.CertificateException; import java.security.cert.X509Certificate; @@ -8,7 +7,6 @@ import java.security.cert.X509Certificate; import javax.net.ssl.X509TrustManager; -@SuppressLint("TrustAllX509TrustManager") class VeryTrustingTrustManager implements X509TrustManager { private final X509Certificate serverCertificate; -- GitLab From 906cc19b6ef3dce4dcd5dbe77102513cc662fa2f Mon Sep 17 00:00:00 2001 From: cketti Date: Thu, 28 Apr 2022 23:23:44 +0200 Subject: [PATCH 36/75] Replace usage of `JSONObject` with Moshi --- mail/common/build.gradle | 1 + .../k9/mail/oauth/XOAuth2ChallengeParser.java | 19 ++++++++++++------- .../fsck/k9/mail/oauth/XOAuth2Response.java | 6 ++++++ 3 files changed, 19 insertions(+), 7 deletions(-) create mode 100644 mail/common/src/main/java/com/fsck/k9/mail/oauth/XOAuth2Response.java diff --git a/mail/common/build.gradle b/mail/common/build.gradle index 5ce857634f..181c285e18 100644 --- a/mail/common/build.gradle +++ b/mail/common/build.gradle @@ -11,6 +11,7 @@ dependencies { implementation "com.squareup.okio:okio:${versions.okio}" implementation "commons-io:commons-io:${versions.commonsIo}" implementation "com.jakewharton.timber:timber:${versions.timber}" + implementation "com.squareup.moshi:moshi:${versions.moshi}" testImplementation project(":mail:testing") testImplementation "org.robolectric:robolectric:${versions.robolectric}" diff --git a/mail/common/src/main/java/com/fsck/k9/mail/oauth/XOAuth2ChallengeParser.java b/mail/common/src/main/java/com/fsck/k9/mail/oauth/XOAuth2ChallengeParser.java index 9ec8665d35..a8523614d3 100644 --- a/mail/common/src/main/java/com/fsck/k9/mail/oauth/XOAuth2ChallengeParser.java +++ b/mail/common/src/main/java/com/fsck/k9/mail/oauth/XOAuth2ChallengeParser.java @@ -1,10 +1,13 @@ package com.fsck.k9.mail.oauth; +import java.io.IOException; + import com.fsck.k9.mail.K9MailLib; import com.fsck.k9.mail.filter.Base64; -import org.json.JSONException; -import org.json.JSONObject; +import com.squareup.moshi.JsonAdapter; +import com.squareup.moshi.JsonDataException; +import com.squareup.moshi.Moshi; import timber.log.Timber; @@ -24,13 +27,15 @@ public class XOAuth2ChallengeParser { } try { - JSONObject json = new JSONObject(decodedResponse); - String status = json.getString("status"); - if (!BAD_RESPONSE.equals(status)) { + Moshi moshi = new Moshi.Builder().build(); + JsonAdapter adapter = moshi.adapter(XOAuth2Response.class); + XOAuth2Response responseObject = adapter.fromJson(decodedResponse); + if (responseObject != null && responseObject.status != null && + !BAD_RESPONSE.equals(responseObject.status)) { return false; } - } catch (JSONException jsonException) { - Timber.e("Error decoding JSON response from: %s. Response was: %s", host, decodedResponse); + } catch (IOException | JsonDataException e) { + Timber.e(e, "Error decoding JSON response from: %s. Response was: %s", host, decodedResponse); } return true; diff --git a/mail/common/src/main/java/com/fsck/k9/mail/oauth/XOAuth2Response.java b/mail/common/src/main/java/com/fsck/k9/mail/oauth/XOAuth2Response.java new file mode 100644 index 0000000000..40d9b04896 --- /dev/null +++ b/mail/common/src/main/java/com/fsck/k9/mail/oauth/XOAuth2Response.java @@ -0,0 +1,6 @@ +package com.fsck.k9.mail.oauth; + + +class XOAuth2Response { + public String status; +} -- GitLab From 0a05802843a7a07e2410f2ea47f4ebc695064962 Mon Sep 17 00:00:00 2001 From: cketti Date: Sun, 1 May 2022 01:25:05 +0200 Subject: [PATCH 37/75] Use our standard read/connect timeouts in `SmtpTransport` --- mail/common/src/main/java/com/fsck/k9/mail/Transport.java | 6 ------ .../java/com/fsck/k9/mail/transport/smtp/SmtpTransport.kt | 2 ++ 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/mail/common/src/main/java/com/fsck/k9/mail/Transport.java b/mail/common/src/main/java/com/fsck/k9/mail/Transport.java index 6b60e52437..6fdf523ecf 100644 --- a/mail/common/src/main/java/com/fsck/k9/mail/Transport.java +++ b/mail/common/src/main/java/com/fsck/k9/mail/Transport.java @@ -2,12 +2,6 @@ package com.fsck.k9.mail; public abstract class Transport { - - protected static final int SOCKET_CONNECT_TIMEOUT = 10000; - - // RFC 1047 - protected static final int SOCKET_READ_TIMEOUT = 300000; - public abstract void open() throws MessagingException; public abstract void sendMessage(Message message) throws MessagingException; diff --git a/mail/protocols/smtp/src/main/java/com/fsck/k9/mail/transport/smtp/SmtpTransport.kt b/mail/protocols/smtp/src/main/java/com/fsck/k9/mail/transport/smtp/SmtpTransport.kt index fbf7383b3e..87570d0bdc 100644 --- a/mail/protocols/smtp/src/main/java/com/fsck/k9/mail/transport/smtp/SmtpTransport.kt +++ b/mail/protocols/smtp/src/main/java/com/fsck/k9/mail/transport/smtp/SmtpTransport.kt @@ -10,6 +10,8 @@ import com.fsck.k9.mail.K9MailLib import com.fsck.k9.mail.Message import com.fsck.k9.mail.Message.RecipientType import com.fsck.k9.mail.MessagingException +import com.fsck.k9.mail.NetworkTimeouts.SOCKET_CONNECT_TIMEOUT +import com.fsck.k9.mail.NetworkTimeouts.SOCKET_READ_TIMEOUT import com.fsck.k9.mail.ServerSettings import com.fsck.k9.mail.Transport import com.fsck.k9.mail.filter.Base64 -- GitLab From 1280f43cba8fb720528cce54ec0ac2f668482558 Mon Sep 17 00:00:00 2001 From: cketti Date: Sun, 1 May 2022 01:31:46 +0200 Subject: [PATCH 38/75] Fix STARTTLS bug in `SmtpTransport` --- .../k9/mail/transport/smtp/SmtpTransport.kt | 7 +++-- .../transport/mockServer/MockSmtpServer.java | 10 +++++++ .../mail/transport/smtp/SmtpTransportTest.kt | 29 +++++++++++++++++++ 3 files changed, 43 insertions(+), 3 deletions(-) diff --git a/mail/protocols/smtp/src/main/java/com/fsck/k9/mail/transport/smtp/SmtpTransport.kt b/mail/protocols/smtp/src/main/java/com/fsck/k9/mail/transport/smtp/SmtpTransport.kt index 87570d0bdc..2c7e1f191f 100644 --- a/mail/protocols/smtp/src/main/java/com/fsck/k9/mail/transport/smtp/SmtpTransport.kt +++ b/mail/protocols/smtp/src/main/java/com/fsck/k9/mail/transport/smtp/SmtpTransport.kt @@ -130,15 +130,16 @@ class SmtpTransport( if (extensions.containsKey("STARTTLS")) { executeCommand("STARTTLS") - this.socket = trustedSocketFactory.createSocket( + val tlsSocket = trustedSocketFactory.createSocket( socket, host, port, clientCertificateAlias ) - inputStream = PeekableInputStream(BufferedInputStream(socket.getInputStream(), 1024)) + this.socket = tlsSocket + inputStream = PeekableInputStream(BufferedInputStream(tlsSocket.getInputStream(), 1024)) responseParser = SmtpResponseParser(logger, inputStream!!) - outputStream = BufferedOutputStream(socket.getOutputStream(), 1024) + outputStream = BufferedOutputStream(tlsSocket.getOutputStream(), 1024) // Now resend the EHLO. Required by RFC2487 Sec. 5.2, and more specifically, Exim. extensions = sendHello(helloName) diff --git a/mail/protocols/smtp/src/test/java/com/fsck/k9/mail/transport/mockServer/MockSmtpServer.java b/mail/protocols/smtp/src/test/java/com/fsck/k9/mail/transport/mockServer/MockSmtpServer.java index a4a994f9fb..35ece7ab7c 100644 --- a/mail/protocols/smtp/src/test/java/com/fsck/k9/mail/transport/mockServer/MockSmtpServer.java +++ b/mail/protocols/smtp/src/test/java/com/fsck/k9/mail/transport/mockServer/MockSmtpServer.java @@ -68,6 +68,11 @@ public class MockSmtpServer { interactions.add(new ExpectedCommand(command)); } + public void startTls() { + checkServerNotRunning(); + interactions.add(new UpgradeToTls()); + } + public void closeConnection() { checkServerNotRunning(); interactions.add(new CloseConnection()); @@ -212,6 +217,9 @@ public class MockSmtpServer { } } + private static class UpgradeToTls implements SmtpInteraction { + } + private static class CloseConnection implements SmtpInteraction { } @@ -303,6 +311,8 @@ public class MockSmtpServer { readExpectedCommand((ExpectedCommand) interaction); } else if (interaction instanceof CannedResponse) { writeCannedResponse((CannedResponse) interaction); + } else if (interaction instanceof UpgradeToTls) { + upgradeToTls(socket); } else if (interaction instanceof CloseConnection) { clientSocket.close(); } diff --git a/mail/protocols/smtp/src/test/java/com/fsck/k9/mail/transport/smtp/SmtpTransportTest.kt b/mail/protocols/smtp/src/test/java/com/fsck/k9/mail/transport/smtp/SmtpTransportTest.kt index 6c3e7b196d..ed9ae481d4 100644 --- a/mail/protocols/smtp/src/test/java/com/fsck/k9/mail/transport/smtp/SmtpTransportTest.kt +++ b/mail/protocols/smtp/src/test/java/com/fsck/k9/mail/transport/smtp/SmtpTransportTest.kt @@ -567,6 +567,35 @@ class SmtpTransportTest { server.verifyInteractionCompleted() } + @Test + fun `open() with STARTTLS`() { + val server = MockSmtpServer().apply { + output("220 localhost Simple Mail Transfer Service Ready") + expect("EHLO [127.0.0.1]") + output("250-localhost Hello 127.0.0.1") + output("250-STARTTLS") + output("250 HELP") + expect("STARTTLS") + output("220 Ready to start TLS") + startTls() + expect("EHLO [127.0.0.1]") + output("250-localhost Hello 127.0.0.1") + output("250 AUTH PLAIN LOGIN") + expect("AUTH PLAIN AHVzZXIAcGFzc3dvcmQ=") + output("235 2.7.0 Authentication successful") + } + val transport = startServerAndCreateSmtpTransport( + server, + authenticationType = AuthType.PLAIN, + connectionSecurity = ConnectionSecurity.STARTTLS_REQUIRED + ) + + transport.open() + + server.verifyConnectionStillOpen() + server.verifyInteractionCompleted() + } + @Test fun `sendMessage() without address to send to should not open connection`() { val message = MimeMessage() -- GitLab From 2abe7d2b9fc22dcf8c36272bcbe78de075dd105a Mon Sep 17 00:00:00 2001 From: cketti Date: Sun, 1 May 2022 23:22:00 +0200 Subject: [PATCH 39/75] Replace usage of `android.text.TextUtils` in `Address` --- .../src/main/java/com/fsck/k9/mail/Address.java | 2 +- .../main/java/com/fsck/k9/mail/helper/TextUtils.kt | 11 +++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) create mode 100644 mail/common/src/main/java/com/fsck/k9/mail/helper/TextUtils.kt diff --git a/mail/common/src/main/java/com/fsck/k9/mail/Address.java b/mail/common/src/main/java/com/fsck/k9/mail/Address.java index 587ce7a262..fdaa1d9f71 100644 --- a/mail/common/src/main/java/com/fsck/k9/mail/Address.java +++ b/mail/common/src/main/java/com/fsck/k9/mail/Address.java @@ -7,6 +7,7 @@ import java.util.ArrayList; import java.util.List; import java.util.regex.Pattern; +import com.fsck.k9.mail.helper.TextUtils; import org.apache.james.mime4j.MimeException; import org.apache.james.mime4j.codec.DecodeMonitor; import org.apache.james.mime4j.codec.EncoderUtil; @@ -17,7 +18,6 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.VisibleForTesting; import timber.log.Timber; -import android.text.TextUtils; import android.text.util.Rfc822Token; import android.text.util.Rfc822Tokenizer; diff --git a/mail/common/src/main/java/com/fsck/k9/mail/helper/TextUtils.kt b/mail/common/src/main/java/com/fsck/k9/mail/helper/TextUtils.kt new file mode 100644 index 0000000000..44fdf305e3 --- /dev/null +++ b/mail/common/src/main/java/com/fsck/k9/mail/helper/TextUtils.kt @@ -0,0 +1,11 @@ +package com.fsck.k9.mail.helper + +object TextUtils { + @JvmStatic + fun isEmpty(text: String?) = text.isNullOrEmpty() + + @JvmStatic + fun join(separator: String, items: Array): String { + return items.joinToString(separator) + } +} -- GitLab From c61dc117d2438e9491511c163adee3623a62c475 Mon Sep 17 00:00:00 2001 From: cketti Date: Sun, 1 May 2022 23:39:36 +0200 Subject: [PATCH 40/75] Replace usage of `android.text.util.Rfc822Token[izer]` in `Address` At some point we need to clean up our email address parser mess. But for now we just copy Android's implementation of `Rfc822Token` and `Rfc822Tokenizer`. --- .../main/java/com/fsck/k9/mail/Address.java | 5 +- .../com/fsck/k9/mail/helper/Rfc822Token.java | 205 ++++++++++++ .../fsck/k9/mail/helper/Rfc822Tokenizer.java | 313 ++++++++++++++++++ 3 files changed, 520 insertions(+), 3 deletions(-) create mode 100644 mail/common/src/main/java/com/fsck/k9/mail/helper/Rfc822Token.java create mode 100644 mail/common/src/main/java/com/fsck/k9/mail/helper/Rfc822Tokenizer.java diff --git a/mail/common/src/main/java/com/fsck/k9/mail/Address.java b/mail/common/src/main/java/com/fsck/k9/mail/Address.java index fdaa1d9f71..d1382c866f 100644 --- a/mail/common/src/main/java/com/fsck/k9/mail/Address.java +++ b/mail/common/src/main/java/com/fsck/k9/mail/Address.java @@ -7,6 +7,8 @@ import java.util.ArrayList; import java.util.List; import java.util.regex.Pattern; +import com.fsck.k9.mail.helper.Rfc822Token; +import com.fsck.k9.mail.helper.Rfc822Tokenizer; import com.fsck.k9.mail.helper.TextUtils; import org.apache.james.mime4j.MimeException; import org.apache.james.mime4j.codec.DecodeMonitor; @@ -18,9 +20,6 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.VisibleForTesting; import timber.log.Timber; -import android.text.util.Rfc822Token; -import android.text.util.Rfc822Tokenizer; - public class Address implements Serializable { private static final Pattern ATOM = Pattern.compile("^(?:[a-zA-Z0-9!#$%&'*+\\-/=?^_`{|}~]|\\s)+$"); diff --git a/mail/common/src/main/java/com/fsck/k9/mail/helper/Rfc822Token.java b/mail/common/src/main/java/com/fsck/k9/mail/helper/Rfc822Token.java new file mode 100644 index 0000000000..bf0721f910 --- /dev/null +++ b/mail/common/src/main/java/com/fsck/k9/mail/helper/Rfc822Token.java @@ -0,0 +1,205 @@ +/* + * Copyright (C) 2008 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.helper; + + +import org.jetbrains.annotations.Nullable; + + +/** + * This class stores an RFC 822-like name, address, and comment, + * and provides methods to convert them to quoted strings. + */ +public class Rfc822Token { + @Nullable + private String mName, mAddress, mComment; + + /** + * Creates a new Rfc822Token with the specified name, address, + * and comment. + */ + public Rfc822Token(@Nullable String name, @Nullable String address, @Nullable String comment) { + mName = name; + mAddress = address; + mComment = comment; + } + + /** + * Returns the name part. + */ + @Nullable + public String getName() { + return mName; + } + + /** + * Returns the address part. + */ + @Nullable + public String getAddress() { + return mAddress; + } + + /** + * Returns the comment part. + */ + @Nullable + public String getComment() { + return mComment; + } + + /** + * Changes the name to the specified name. + */ + public void setName(@Nullable String name) { + mName = name; + } + + /** + * Changes the address to the specified address. + */ + public void setAddress(@Nullable String address) { + mAddress = address; + } + + /** + * Changes the comment to the specified comment. + */ + public void setComment(@Nullable String comment) { + mComment = comment; + } + + /** + * Returns the name (with quoting added if necessary), + * the comment (in parentheses), and the address (in angle brackets). + * This should be suitable for inclusion in an RFC 822 address list. + */ + public String toString() { + StringBuilder sb = new StringBuilder(); + + if (mName != null && mName.length() != 0) { + sb.append(quoteNameIfNecessary(mName)); + sb.append(' '); + } + + if (mComment != null && mComment.length() != 0) { + sb.append('('); + sb.append(quoteComment(mComment)); + sb.append(") "); + } + + if (mAddress != null && mAddress.length() != 0) { + sb.append('<'); + sb.append(mAddress); + sb.append('>'); + } + + return sb.toString(); + } + + /** + * Returns the name, conservatively quoting it if there are any + * characters that are likely to cause trouble outside of a + * quoted string, or returning it literally if it seems safe. + */ + public static String quoteNameIfNecessary(String name) { + int len = name.length(); + + for (int i = 0; i < len; i++) { + char c = name.charAt(i); + + if (! ((c >= 'A' && c <= 'Z') || + (c >= 'a' && c <= 'z') || + (c == ' ') || + (c >= '0' && c <= '9'))) { + return '"' + quoteName(name) + '"'; + } + } + + return name; + } + + /** + * Returns the name, with internal backslashes and quotation marks + * preceded by backslashes. The outer quote marks themselves are not + * added by this method. + */ + public static String quoteName(String name) { + StringBuilder sb = new StringBuilder(); + + int len = name.length(); + for (int i = 0; i < len; i++) { + char c = name.charAt(i); + + if (c == '\\' || c == '"') { + sb.append('\\'); + } + + sb.append(c); + } + + return sb.toString(); + } + + /** + * Returns the comment, with internal backslashes and parentheses + * preceded by backslashes. The outer parentheses themselves are + * not added by this method. + */ + public static String quoteComment(String comment) { + int len = comment.length(); + StringBuilder sb = new StringBuilder(); + + for (int i = 0; i < len; i++) { + char c = comment.charAt(i); + + if (c == '(' || c == ')' || c == '\\') { + sb.append('\\'); + } + + sb.append(c); + } + + return sb.toString(); + } + + public int hashCode() { + int result = 17; + if (mName != null) result = 31 * result + mName.hashCode(); + if (mAddress != null) result = 31 * result + mAddress.hashCode(); + if (mComment != null) result = 31 * result + mComment.hashCode(); + return result; + } + + private static boolean stringEquals(String a, String b) { + if (a == null) { + return (b == null); + } else { + return (a.equals(b)); + } + } + + public boolean equals(@Nullable Object o) { + if (!(o instanceof Rfc822Token)) { + return false; + } + Rfc822Token other = (Rfc822Token) o; + return (stringEquals(mName, other.mName) && + stringEquals(mAddress, other.mAddress) && + stringEquals(mComment, other.mComment)); + } +} diff --git a/mail/common/src/main/java/com/fsck/k9/mail/helper/Rfc822Tokenizer.java b/mail/common/src/main/java/com/fsck/k9/mail/helper/Rfc822Tokenizer.java new file mode 100644 index 0000000000..faf640ab01 --- /dev/null +++ b/mail/common/src/main/java/com/fsck/k9/mail/helper/Rfc822Tokenizer.java @@ -0,0 +1,313 @@ +/* + * Copyright (C) 2008 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.helper; + + +import java.util.ArrayList; +import java.util.Collection; + +/** + * This class works as a Tokenizer for MultiAutoCompleteTextView for + * address list fields, and also provides a method for converting + * a string of addresses (such as might be typed into such a field) + * into a series of Rfc822Tokens. + */ +public class Rfc822Tokenizer { + + /** + * This constructor will try to take a string like + * "Foo Bar (something) <foo\@google.com>, + * blah\@google.com (something)" + * and convert it into one or more Rfc822Tokens, output into the supplied + * collection. + * + * It does *not* decode MIME encoded-words; charset conversion + * must already have taken place if necessary. + * It will try to be tolerant of broken syntax instead of + * returning an error. + * + */ + public static void tokenize(CharSequence text, Collection out) { + StringBuilder name = new StringBuilder(); + StringBuilder address = new StringBuilder(); + StringBuilder comment = new StringBuilder(); + + int i = 0; + int cursor = text.length(); + + while (i < cursor) { + char c = text.charAt(i); + + if (c == ',' || c == ';') { + i++; + + while (i < cursor && text.charAt(i) == ' ') { + i++; + } + + crunch(name); + + if (address.length() > 0) { + out.add(new Rfc822Token(name.toString(), + address.toString(), + comment.toString())); + } else if (name.length() > 0) { + out.add(new Rfc822Token(null, + name.toString(), + comment.toString())); + } + + name.setLength(0); + address.setLength(0); + comment.setLength(0); + } else if (c == '"') { + i++; + + while (i < cursor) { + c = text.charAt(i); + + if (c == '"') { + i++; + break; + } else if (c == '\\') { + if (i + 1 < cursor) { + name.append(text.charAt(i + 1)); + } + i += 2; + } else { + name.append(c); + i++; + } + } + } else if (c == '(') { + int level = 1; + i++; + + while (i < cursor && level > 0) { + c = text.charAt(i); + + if (c == ')') { + if (level > 1) { + comment.append(c); + } + + level--; + i++; + } else if (c == '(') { + comment.append(c); + level++; + i++; + } else if (c == '\\') { + if (i + 1 < cursor) { + comment.append(text.charAt(i + 1)); + } + i += 2; + } else { + comment.append(c); + i++; + } + } + } else if (c == '<') { + i++; + + while (i < cursor) { + c = text.charAt(i); + + if (c == '>') { + i++; + break; + } else { + address.append(c); + i++; + } + } + } else if (c == ' ') { + name.append('\0'); + i++; + } else { + name.append(c); + i++; + } + } + + crunch(name); + + if (address.length() > 0) { + out.add(new Rfc822Token(name.toString(), + address.toString(), + comment.toString())); + } else if (name.length() > 0) { + out.add(new Rfc822Token(null, + name.toString(), + comment.toString())); + } + } + + /** + * This method will try to take a string like + * "Foo Bar (something) <foo\@google.com>, + * blah\@google.com (something)" + * and convert it into one or more Rfc822Tokens. + * It does *not* decode MIME encoded-words; charset conversion + * must already have taken place if necessary. + * It will try to be tolerant of broken syntax instead of + * returning an error. + */ + public static Rfc822Token[] tokenize(CharSequence text) { + ArrayList out = new ArrayList(); + tokenize(text, out); + return out.toArray(new Rfc822Token[out.size()]); + } + + private static void crunch(StringBuilder sb) { + int i = 0; + int len = sb.length(); + + while (i < len) { + char c = sb.charAt(i); + + if (c == '\0') { + if (i == 0 || i == len - 1 || + sb.charAt(i - 1) == ' ' || + sb.charAt(i - 1) == '\0' || + sb.charAt(i + 1) == ' ' || + sb.charAt(i + 1) == '\0') { + sb.deleteCharAt(i); + len--; + } else { + i++; + } + } else { + i++; + } + } + + for (i = 0; i < len; i++) { + if (sb.charAt(i) == '\0') { + sb.setCharAt(i, ' '); + } + } + } + + /** + * {@inheritDoc} + */ + public int findTokenStart(CharSequence text, int cursor) { + /* + * It's hard to search backward, so search forward until + * we reach the cursor. + */ + + int best = 0; + int i = 0; + + while (i < cursor) { + i = findTokenEnd(text, i); + + if (i < cursor) { + i++; // Skip terminating punctuation + + while (i < cursor && text.charAt(i) == ' ') { + i++; + } + + if (i < cursor) { + best = i; + } + } + } + + return best; + } + + /** + * {@inheritDoc} + */ + public int findTokenEnd(CharSequence text, int cursor) { + int len = text.length(); + int i = cursor; + + while (i < len) { + char c = text.charAt(i); + + if (c == ',' || c == ';') { + return i; + } else if (c == '"') { + i++; + + while (i < len) { + c = text.charAt(i); + + if (c == '"') { + i++; + break; + } else if (c == '\\' && i + 1 < len) { + i += 2; + } else { + i++; + } + } + } else if (c == '(') { + int level = 1; + i++; + + while (i < len && level > 0) { + c = text.charAt(i); + + if (c == ')') { + level--; + i++; + } else if (c == '(') { + level++; + i++; + } else if (c == '\\' && i + 1 < len) { + i += 2; + } else { + i++; + } + } + } else if (c == '<') { + i++; + + while (i < len) { + c = text.charAt(i); + + if (c == '>') { + i++; + break; + } else { + i++; + } + } + } else { + i++; + } + } + + return i; + } + + /** + * Terminates the specified address with a comma and space. + * This assumes that the specified text already has valid syntax. + * The Adapter subclass's convertToString() method must make that + * guarantee. + */ + public CharSequence terminateToken(CharSequence text) { + return text + ", "; + } +} -- GitLab From ff48609b6f3cde475db0a8be15d9f2256bf6b214 Mon Sep 17 00:00:00 2001 From: cketti Date: Mon, 2 May 2022 00:12:31 +0200 Subject: [PATCH 41/75] Remove `CertificateValidationException`'s dependency on the Android SDK --- .../java/com/fsck/k9/mail/CertificateValidationException.java | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/mail/common/src/main/java/com/fsck/k9/mail/CertificateValidationException.java b/mail/common/src/main/java/com/fsck/k9/mail/CertificateValidationException.java index 18bd457f18..43628424df 100644 --- a/mail/common/src/main/java/com/fsck/k9/mail/CertificateValidationException.java +++ b/mail/common/src/main/java/com/fsck/k9/mail/CertificateValidationException.java @@ -7,8 +7,6 @@ import java.security.cert.X509Certificate; import javax.net.ssl.SSLHandshakeException; -import android.security.KeyChainException; - public class CertificateValidationException extends MessagingException { public static final long serialVersionUID = -1; private final Reason mReason; @@ -90,7 +88,7 @@ public class CertificateValidationException extends MessagingException { while (throwable != null && !(throwable instanceof CertPathValidatorException) && !(throwable instanceof CertificateException) - && !(throwable instanceof KeyChainException) + && !("android.security.KeyChainException".equals(throwable.getClass().getCanonicalName())) && !(throwable instanceof SSLHandshakeException)) { throwable = throwable.getCause(); } -- GitLab From d69c1f4c4610d8b00c3ec7fdff7223bcca804835 Mon Sep 17 00:00:00 2001 From: cketti Date: Mon, 2 May 2022 01:53:00 +0200 Subject: [PATCH 42/75] Add simple logging abstraction Once there's a JVM artifact for Timber, hopefully all we have to do is replace the imports again. --- app/core/src/main/java/com/fsck/k9/K9.kt | 1 + .../src/main/java/com/fsck/k9/TimberLogger.kt | 121 ++++++++++++++++++ build.gradle | 1 + mail/common/build.gradle | 3 +- .../main/java/com/fsck/k9/logging/Logger.kt | 26 ++++ .../java/com/fsck/k9/logging/NoOpLogger.kt | 36 ++++++ .../main/java/com/fsck/k9/logging/Timber.kt | 83 ++++++++++++ .../main/java/com/fsck/k9/mail/Address.java | 2 +- .../main/java/com/fsck/k9/mail/Message.java | 2 +- .../k9/mail/internet/BinaryTempFileBody.java | 4 +- .../fsck/k9/mail/internet/CharsetSupport.java | 3 +- .../com/fsck/k9/mail/internet/DecoderUtil.kt | 2 +- .../k9/mail/internet/MessageExtractor.java | 2 +- .../fsck/k9/mail/internet/MimeMessage.java | 2 +- .../fsck/k9/mail/internet/MimeUtility.java | 2 +- .../k9/mail/oauth/XOAuth2ChallengeParser.java | 2 +- .../com/fsck/k9/mail/ssl/LocalKeyStore.kt | 2 +- .../fsck/k9/mail/ssl/TrustManagerFactory.java | 2 +- mail/protocols/imap/build.gradle | 1 - .../mail/store/imap/ImapResponseParser.java | 2 +- .../fsck/k9/mail/store/imap/ImapUtility.java | 2 +- .../mail/store/imap/RealImapConnection.java | 2 +- .../fsck/k9/mail/store/imap/RealImapFolder.kt | 2 +- .../k9/mail/store/imap/RealImapFolderIdler.kt | 2 +- .../k9/mail/store/imap/RealImapStore.java | 2 +- mail/protocols/pop3/build.gradle | 2 - .../k9/mail/store/pop3/Pop3Connection.java | 2 +- .../fsck/k9/mail/store/pop3/Pop3Folder.java | 2 +- mail/protocols/smtp/build.gradle | 1 - .../k9/mail/transport/smtp/SmtpTransport.kt | 2 +- mail/protocols/webdav/build.gradle | 1 - .../fsck/k9/mail/store/webdav/DataSet.java | 3 +- .../k9/mail/store/webdav/HttpGeneric.java | 4 +- .../k9/mail/store/webdav/WebDavFolder.java | 2 +- .../mail/store/webdav/WebDavHttpClient.java | 2 +- .../k9/mail/store/webdav/WebDavMessage.java | 3 +- .../k9/mail/store/webdav/WebDavStore.java | 2 +- .../k9/mail/transport/WebDavTransport.java | 2 +- 38 files changed, 300 insertions(+), 37 deletions(-) create mode 100644 app/core/src/main/java/com/fsck/k9/TimberLogger.kt create mode 100644 mail/common/src/main/java/com/fsck/k9/logging/Logger.kt create mode 100644 mail/common/src/main/java/com/fsck/k9/logging/NoOpLogger.kt create mode 100644 mail/common/src/main/java/com/fsck/k9/logging/Timber.kt diff --git a/app/core/src/main/java/com/fsck/k9/K9.kt b/app/core/src/main/java/com/fsck/k9/K9.kt index 20826b4ffe..d68f78431d 100644 --- a/app/core/src/main/java/com/fsck/k9/K9.kt +++ b/app/core/src/main/java/com/fsck/k9/K9.kt @@ -285,6 +285,7 @@ object K9 : EarlyInit { override fun debugSensitive(): Boolean = isSensitiveDebugLoggingEnabled }) + com.fsck.k9.logging.Timber.logger = TimberLogger() checkCachedDatabaseVersion(context) diff --git a/app/core/src/main/java/com/fsck/k9/TimberLogger.kt b/app/core/src/main/java/com/fsck/k9/TimberLogger.kt new file mode 100644 index 0000000000..80c1af8c50 --- /dev/null +++ b/app/core/src/main/java/com/fsck/k9/TimberLogger.kt @@ -0,0 +1,121 @@ +package com.fsck.k9 + +import android.os.Build +import com.fsck.k9.logging.Logger +import java.util.regex.Pattern +import timber.log.Timber + +class TimberLogger : Logger { + override fun v(message: String?, vararg args: Any?) { + setTimberTag() + Timber.v(message, *args) + } + + override fun v(t: Throwable?, message: String?, vararg args: Any?) { + setTimberTag() + Timber.v(t, message, *args) + } + + override fun v(t: Throwable?) { + setTimberTag() + Timber.v(t) + } + + override fun d(message: String?, vararg args: Any?) { + setTimberTag() + Timber.d(message, *args) + } + + override fun d(t: Throwable?, message: String?, vararg args: Any?) { + setTimberTag() + Timber.d(t, message, *args) + } + + override fun d(t: Throwable?) { + setTimberTag() + Timber.d(t) + } + + override fun i(message: String?, vararg args: Any?) { + setTimberTag() + Timber.i(message, *args) + } + + override fun i(t: Throwable?, message: String?, vararg args: Any?) { + setTimberTag() + Timber.i(t, message, *args) + } + + override fun i(t: Throwable?) { + setTimberTag() + Timber.i(t) + } + + override fun w(message: String?, vararg args: Any?) { + setTimberTag() + Timber.w(message, *args) + } + + override fun w(t: Throwable?, message: String?, vararg args: Any?) { + setTimberTag() + Timber.w(t, message, *args) + } + + override fun w(t: Throwable?) { + setTimberTag() + Timber.w(t) + } + + override fun e(message: String?, vararg args: Any?) { + setTimberTag() + Timber.e(message, *args) + } + + override fun e(t: Throwable?, message: String?, vararg args: Any?) { + setTimberTag() + Timber.e(t, message, *args) + } + + override fun e(t: Throwable?) { + setTimberTag() + Timber.e(t) + } + + private fun setTimberTag() { + val tag = Throwable().stackTrace + .first { it.className !in IGNORE_CLASSES } + .let(::createStackElementTag) + + // We explicitly set a tag, otherwise Timber will always derive the tag "TimberLogger". + Timber.tag(tag) + } + + private fun createStackElementTag(element: StackTraceElement): String { + var tag = element.className.substringAfterLast('.') + val matcher = ANONYMOUS_CLASS.matcher(tag) + if (matcher.find()) { + tag = matcher.replaceAll("") + } + + // Tag length limit was removed in API 26. + return if (tag.length <= MAX_TAG_LENGTH || Build.VERSION.SDK_INT >= 26) { + tag + } else { + tag.substring(0, MAX_TAG_LENGTH) + } + } + + companion object { + private const val MAX_TAG_LENGTH = 23 + private val ANONYMOUS_CLASS = Pattern.compile("(\\$\\d+)+$") + + private val IGNORE_CLASSES = setOf( + Timber::class.java.name, + Timber.Forest::class.java.name, + Timber.Tree::class.java.name, + Timber.DebugTree::class.java.name, + TimberLogger::class.java.name, + com.fsck.k9.logging.Timber::class.java.name + ) + } +} diff --git a/build.gradle b/build.gradle index 6a02b28559..e09c158fc3 100644 --- a/build.gradle +++ b/build.gradle @@ -13,6 +13,7 @@ buildscript { versions = [ 'kotlin': '1.6.10', 'kotlinCoroutines': '1.6.0', + 'jetbrainsAnnotations': '23.0.0', 'androidxAppCompat': '1.4.1', 'androidxActivity': '1.4.0', 'androidxRecyclerView': '1.2.1', diff --git a/mail/common/build.gradle b/mail/common/build.gradle index 181c285e18..55ddff0df5 100644 --- a/mail/common/build.gradle +++ b/mail/common/build.gradle @@ -6,11 +6,12 @@ if (rootProject.testCoverage) { } dependencies { + api "org.jetbrains:annotations:${versions.jetbrainsAnnotations}" + implementation "org.apache.james:apache-mime4j-core:${versions.mime4j}" implementation "org.apache.james:apache-mime4j-dom:${versions.mime4j}" implementation "com.squareup.okio:okio:${versions.okio}" implementation "commons-io:commons-io:${versions.commonsIo}" - implementation "com.jakewharton.timber:timber:${versions.timber}" implementation "com.squareup.moshi:moshi:${versions.moshi}" testImplementation project(":mail:testing") diff --git a/mail/common/src/main/java/com/fsck/k9/logging/Logger.kt b/mail/common/src/main/java/com/fsck/k9/logging/Logger.kt new file mode 100644 index 0000000000..854e935b4d --- /dev/null +++ b/mail/common/src/main/java/com/fsck/k9/logging/Logger.kt @@ -0,0 +1,26 @@ +package com.fsck.k9.logging + +/** + * Logging abstraction based on Timber. + */ +interface Logger { + fun v(message: String?, vararg args: Any?) + fun v(t: Throwable?, message: String?, vararg args: Any?) + fun v(t: Throwable?) + + fun d(message: String?, vararg args: Any?) + fun d(t: Throwable?, message: String?, vararg args: Any?) + fun d(t: Throwable?) + + fun i(message: String?, vararg args: Any?) + fun i(t: Throwable?, message: String?, vararg args: Any?) + fun i(t: Throwable?) + + fun w(message: String?, vararg args: Any?) + fun w(t: Throwable?, message: String?, vararg args: Any?) + fun w(t: Throwable?) + + fun e(message: String?, vararg args: Any?) + fun e(t: Throwable?, message: String?, vararg args: Any?) + fun e(t: Throwable?) +} diff --git a/mail/common/src/main/java/com/fsck/k9/logging/NoOpLogger.kt b/mail/common/src/main/java/com/fsck/k9/logging/NoOpLogger.kt new file mode 100644 index 0000000000..bc0a10c9f2 --- /dev/null +++ b/mail/common/src/main/java/com/fsck/k9/logging/NoOpLogger.kt @@ -0,0 +1,36 @@ +package com.fsck.k9.logging + +/** + * A [Logger] implementation that does nothing. + */ +class NoOpLogger : Logger { + override fun v(message: String?, vararg args: Any?) = Unit + + override fun v(t: Throwable?, message: String?, vararg args: Any?) = Unit + + override fun v(t: Throwable?) = Unit + + override fun d(message: String?, vararg args: Any?) = Unit + + override fun d(t: Throwable?, message: String?, vararg args: Any?) = Unit + + override fun d(t: Throwable?) = Unit + + override fun i(message: String?, vararg args: Any?) = Unit + + override fun i(t: Throwable?, message: String?, vararg args: Any?) = Unit + + override fun i(t: Throwable?) = Unit + + override fun w(message: String?, vararg args: Any?) = Unit + + override fun w(t: Throwable?, message: String?, vararg args: Any?) = Unit + + override fun w(t: Throwable?) = Unit + + override fun e(message: String?, vararg args: Any?) = Unit + + override fun e(t: Throwable?, message: String?, vararg args: Any?) = Unit + + override fun e(t: Throwable?) = Unit +} diff --git a/mail/common/src/main/java/com/fsck/k9/logging/Timber.kt b/mail/common/src/main/java/com/fsck/k9/logging/Timber.kt new file mode 100644 index 0000000000..e0912ec943 --- /dev/null +++ b/mail/common/src/main/java/com/fsck/k9/logging/Timber.kt @@ -0,0 +1,83 @@ +package com.fsck.k9.logging + +/** + * Our fake `Timber` object. + */ +object Timber { + var logger: Logger = NoOpLogger() + + @JvmStatic + fun v(message: String?, vararg args: Any?) { + logger.v(message, *args) + } + + @JvmStatic + fun v(t: Throwable?, message: String?, vararg args: Any?) { + logger.v(t, message, *args) + } + + @JvmStatic + fun v(t: Throwable?) { + logger.v(t) + } + + @JvmStatic + fun d(message: String?, vararg args: Any?) { + logger.d(message, *args) + } + + @JvmStatic + fun d(t: Throwable?, message: String?, vararg args: Any?) { + logger.d(t, message, *args) + } + + @JvmStatic + fun d(t: Throwable?) { + logger.d(t) + } + + @JvmStatic + fun i(message: String?, vararg args: Any?) { + logger.i(message, *args) + } + + @JvmStatic + fun i(t: Throwable?, message: String?, vararg args: Any?) { + logger.i(t, message, *args) + } + + @JvmStatic + fun i(t: Throwable?) { + logger.i(t) + } + + @JvmStatic + fun w(message: String?, vararg args: Any?) { + logger.w(message, *args) + } + + @JvmStatic + fun w(t: Throwable?, message: String?, vararg args: Any?) { + logger.w(t, message, *args) + } + + @JvmStatic + fun w(t: Throwable?) { + logger.w(t) + } + + @JvmStatic + fun e(message: String?, vararg args: Any?) { + logger.e(message) + } + + @JvmStatic + fun e(t: Throwable?, message: String?, vararg args: Any?) { + logger.e(t, message, *args) + } + + @JvmStatic + fun e(t: Throwable?) { + logger.e(t) + } +} diff --git a/mail/common/src/main/java/com/fsck/k9/mail/Address.java b/mail/common/src/main/java/com/fsck/k9/mail/Address.java index d1382c866f..fd272638fa 100644 --- a/mail/common/src/main/java/com/fsck/k9/mail/Address.java +++ b/mail/common/src/main/java/com/fsck/k9/mail/Address.java @@ -7,6 +7,7 @@ import java.util.ArrayList; import java.util.List; import java.util.regex.Pattern; +import com.fsck.k9.logging.Timber; import com.fsck.k9.mail.helper.Rfc822Token; import com.fsck.k9.mail.helper.Rfc822Tokenizer; import com.fsck.k9.mail.helper.TextUtils; @@ -18,7 +19,6 @@ import org.apache.james.mime4j.dom.address.MailboxList; import org.apache.james.mime4j.field.address.DefaultAddressParser; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.VisibleForTesting; -import timber.log.Timber; public class Address implements Serializable { private static final Pattern ATOM = Pattern.compile("^(?:[a-zA-Z0-9!#$%&'*+\\-/=?^_`{|}~]|\\s)+$"); diff --git a/mail/common/src/main/java/com/fsck/k9/mail/Message.java b/mail/common/src/main/java/com/fsck/k9/mail/Message.java index 91e3a8043c..5ccc21e5c7 100644 --- a/mail/common/src/main/java/com/fsck/k9/mail/Message.java +++ b/mail/common/src/main/java/com/fsck/k9/mail/Message.java @@ -8,10 +8,10 @@ import java.util.EnumSet; import java.util.List; import java.util.Set; +import com.fsck.k9.logging.Timber; import com.fsck.k9.mail.filter.CountingOutputStream; import com.fsck.k9.mail.filter.EOLConvertingOutputStream; import org.jetbrains.annotations.NotNull; -import timber.log.Timber; public abstract class Message implements Part, Body { diff --git a/mail/common/src/main/java/com/fsck/k9/mail/internet/BinaryTempFileBody.java b/mail/common/src/main/java/com/fsck/k9/mail/internet/BinaryTempFileBody.java index 2c664bbfb4..1422b3e95a 100644 --- a/mail/common/src/main/java/com/fsck/k9/mail/internet/BinaryTempFileBody.java +++ b/mail/common/src/main/java/com/fsck/k9/mail/internet/BinaryTempFileBody.java @@ -9,12 +9,12 @@ import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; +import com.fsck.k9.logging.Timber; import com.fsck.k9.mail.MessagingException; import com.fsck.k9.mail.filter.Base64OutputStream; import org.apache.commons.io.IOUtils; import org.apache.james.mime4j.codec.QuotedPrintableOutputStream; import org.apache.james.mime4j.util.MimeUtil; -import timber.log.Timber; /** @@ -146,4 +146,4 @@ public class BinaryTempFileBody implements RawDataBody, SizeAware { super.close(); } } -} \ No newline at end of file +} diff --git a/mail/common/src/main/java/com/fsck/k9/mail/internet/CharsetSupport.java b/mail/common/src/main/java/com/fsck/k9/mail/internet/CharsetSupport.java index f6f0f262aa..038e2636ae 100644 --- a/mail/common/src/main/java/com/fsck/k9/mail/internet/CharsetSupport.java +++ b/mail/common/src/main/java/com/fsck/k9/mail/internet/CharsetSupport.java @@ -1,11 +1,10 @@ package com.fsck.k9.mail.internet; +import com.fsck.k9.logging.Timber; import com.fsck.k9.mail.Message; import com.fsck.k9.mail.MessagingException; -import com.fsck.k9.mail.Part; import org.apache.commons.io.IOUtils; -import timber.log.Timber; import java.io.IOException; import java.io.InputStream; diff --git a/mail/common/src/main/java/com/fsck/k9/mail/internet/DecoderUtil.kt b/mail/common/src/main/java/com/fsck/k9/mail/internet/DecoderUtil.kt index ee8053f1e2..eb5cec8ecf 100644 --- a/mail/common/src/main/java/com/fsck/k9/mail/internet/DecoderUtil.kt +++ b/mail/common/src/main/java/com/fsck/k9/mail/internet/DecoderUtil.kt @@ -1,5 +1,6 @@ package com.fsck.k9.mail.internet +import com.fsck.k9.logging.Timber import com.fsck.k9.mail.Message import com.fsck.k9.mail.MessagingException import java.io.ByteArrayInputStream @@ -11,7 +12,6 @@ import okio.buffer import okio.source import org.apache.james.mime4j.codec.QuotedPrintableInputStream import org.apache.james.mime4j.util.CharsetUtil -import timber.log.Timber /** * Decoder for encoded words (RFC 2047). diff --git a/mail/common/src/main/java/com/fsck/k9/mail/internet/MessageExtractor.java b/mail/common/src/main/java/com/fsck/k9/mail/internet/MessageExtractor.java index c66d73c2c1..3a4e07ddc3 100644 --- a/mail/common/src/main/java/com/fsck/k9/mail/internet/MessageExtractor.java +++ b/mail/common/src/main/java/com/fsck/k9/mail/internet/MessageExtractor.java @@ -10,6 +10,7 @@ import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; +import com.fsck.k9.logging.Timber; import com.fsck.k9.mail.Body; import com.fsck.k9.mail.BodyPart; import com.fsck.k9.mail.Message; @@ -19,7 +20,6 @@ import com.fsck.k9.mail.Part; import org.apache.commons.io.input.BoundedInputStream; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -import timber.log.Timber; import static com.fsck.k9.mail.internet.CharsetSupport.fixupCharset; import static com.fsck.k9.mail.internet.MimeUtility.isSameMimeType; diff --git a/mail/common/src/main/java/com/fsck/k9/mail/internet/MimeMessage.java b/mail/common/src/main/java/com/fsck/k9/mail/internet/MimeMessage.java index 6ec9b65dbf..bc633ceb67 100644 --- a/mail/common/src/main/java/com/fsck/k9/mail/internet/MimeMessage.java +++ b/mail/common/src/main/java/com/fsck/k9/mail/internet/MimeMessage.java @@ -15,6 +15,7 @@ import java.util.List; import java.util.Locale; import java.util.TimeZone; +import com.fsck.k9.logging.Timber; import com.fsck.k9.mail.Address; import com.fsck.k9.mail.Body; import com.fsck.k9.mail.BodyFactory; @@ -37,7 +38,6 @@ import org.apache.james.mime4j.stream.BodyDescriptor; import org.apache.james.mime4j.stream.Field; import org.apache.james.mime4j.stream.MimeConfig; import org.jetbrains.annotations.NotNull; -import timber.log.Timber; /** diff --git a/mail/common/src/main/java/com/fsck/k9/mail/internet/MimeUtility.java b/mail/common/src/main/java/com/fsck/k9/mail/internet/MimeUtility.java index 1991c33a54..dbe0ac785f 100644 --- a/mail/common/src/main/java/com/fsck/k9/mail/internet/MimeUtility.java +++ b/mail/common/src/main/java/com/fsck/k9/mail/internet/MimeUtility.java @@ -9,6 +9,7 @@ import java.util.Locale; import java.util.Map; import java.util.regex.Pattern; +import com.fsck.k9.logging.Timber; import com.fsck.k9.mail.Body; import com.fsck.k9.mail.BodyPart; import com.fsck.k9.mail.Message; @@ -20,7 +21,6 @@ import org.apache.james.mime4j.codec.QuotedPrintableInputStream; import org.apache.james.mime4j.util.MimeUtil; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.VisibleForTesting; -import timber.log.Timber; public class MimeUtility { diff --git a/mail/common/src/main/java/com/fsck/k9/mail/oauth/XOAuth2ChallengeParser.java b/mail/common/src/main/java/com/fsck/k9/mail/oauth/XOAuth2ChallengeParser.java index a8523614d3..550bf4e018 100644 --- a/mail/common/src/main/java/com/fsck/k9/mail/oauth/XOAuth2ChallengeParser.java +++ b/mail/common/src/main/java/com/fsck/k9/mail/oauth/XOAuth2ChallengeParser.java @@ -3,12 +3,12 @@ package com.fsck.k9.mail.oauth; import java.io.IOException; +import com.fsck.k9.logging.Timber; import com.fsck.k9.mail.K9MailLib; import com.fsck.k9.mail.filter.Base64; import com.squareup.moshi.JsonAdapter; import com.squareup.moshi.JsonDataException; import com.squareup.moshi.Moshi; -import timber.log.Timber; /** diff --git a/mail/common/src/main/java/com/fsck/k9/mail/ssl/LocalKeyStore.kt b/mail/common/src/main/java/com/fsck/k9/mail/ssl/LocalKeyStore.kt index a2a13e433c..712fc09d56 100644 --- a/mail/common/src/main/java/com/fsck/k9/mail/ssl/LocalKeyStore.kt +++ b/mail/common/src/main/java/com/fsck/k9/mail/ssl/LocalKeyStore.kt @@ -1,5 +1,6 @@ package com.fsck.k9.mail.ssl +import com.fsck.k9.logging.Timber import java.io.File import java.io.FileInputStream import java.io.FileNotFoundException @@ -11,7 +12,6 @@ import java.security.NoSuchAlgorithmException import java.security.cert.Certificate import java.security.cert.CertificateException import java.security.cert.X509Certificate -import timber.log.Timber private const val KEY_STORE_FILE_VERSION = 1 private val PASSWORD = charArrayOf() diff --git a/mail/common/src/main/java/com/fsck/k9/mail/ssl/TrustManagerFactory.java b/mail/common/src/main/java/com/fsck/k9/mail/ssl/TrustManagerFactory.java index 26f650bf9e..0f6212f372 100644 --- a/mail/common/src/main/java/com/fsck/k9/mail/ssl/TrustManagerFactory.java +++ b/mail/common/src/main/java/com/fsck/k9/mail/ssl/TrustManagerFactory.java @@ -10,12 +10,12 @@ import java.security.cert.X509Certificate; import java.util.HashMap; import java.util.Map; +import com.fsck.k9.logging.Timber; import com.fsck.k9.mail.CertificateChainException; import javax.net.ssl.SSLException; import javax.net.ssl.TrustManager; import javax.net.ssl.X509TrustManager; import org.apache.http.conn.ssl.StrictHostnameVerifier; -import timber.log.Timber; public class TrustManagerFactory { public static TrustManagerFactory createInstance(LocalKeyStore localKeyStore) { diff --git a/mail/protocols/imap/build.gradle b/mail/protocols/imap/build.gradle index 494445041a..683d0155b0 100644 --- a/mail/protocols/imap/build.gradle +++ b/mail/protocols/imap/build.gradle @@ -11,7 +11,6 @@ dependencies { implementation "com.jcraft:jzlib:1.0.7" implementation "com.beetstra.jutf7:jutf7:1.0.0" implementation "commons-io:commons-io:${versions.commonsIo}" - implementation "com.jakewharton.timber:timber:${versions.timber}" testImplementation project(":mail:testing") testImplementation "org.robolectric:robolectric:${versions.robolectric}" diff --git a/mail/protocols/imap/src/main/java/com/fsck/k9/mail/store/imap/ImapResponseParser.java b/mail/protocols/imap/src/main/java/com/fsck/k9/mail/store/imap/ImapResponseParser.java index 7357f01cdb..64c2058006 100644 --- a/mail/protocols/imap/src/main/java/com/fsck/k9/mail/store/imap/ImapResponseParser.java +++ b/mail/protocols/imap/src/main/java/com/fsck/k9/mail/store/imap/ImapResponseParser.java @@ -6,10 +6,10 @@ import java.util.ArrayList; import java.util.Iterator; import java.util.List; +import com.fsck.k9.logging.Timber; import com.fsck.k9.mail.K9MailLib; import com.fsck.k9.mail.filter.FixedLengthInputStream; import com.fsck.k9.mail.filter.PeekableInputStream; -import timber.log.Timber; import static com.fsck.k9.mail.K9MailLib.DEBUG_PROTOCOL_IMAP; 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 index c619a6d9ea..1c241e73cd 100644 --- 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 @@ -22,8 +22,8 @@ import java.util.ArrayList; import java.util.Collection; import java.util.List; +import com.fsck.k9.logging.Timber; import com.fsck.k9.mail.Flag; -import timber.log.Timber; /** diff --git a/mail/protocols/imap/src/main/java/com/fsck/k9/mail/store/imap/RealImapConnection.java b/mail/protocols/imap/src/main/java/com/fsck/k9/mail/store/imap/RealImapConnection.java index b9f8095e64..dc8df2fb4a 100644 --- a/mail/protocols/imap/src/main/java/com/fsck/k9/mail/store/imap/RealImapConnection.java +++ b/mail/protocols/imap/src/main/java/com/fsck/k9/mail/store/imap/RealImapConnection.java @@ -27,6 +27,7 @@ import java.util.regex.Pattern; import java.util.zip.Inflater; import java.util.zip.InflaterInputStream; +import com.fsck.k9.logging.Timber; import com.fsck.k9.mail.Authentication; import com.fsck.k9.mail.AuthenticationFailedException; import com.fsck.k9.mail.CertificateValidationException; @@ -45,7 +46,6 @@ import javax.net.ssl.SSLException; import org.apache.commons.io.IOUtils; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -import timber.log.Timber; import static com.fsck.k9.mail.ConnectionSecurity.STARTTLS_REQUIRED; import static com.fsck.k9.mail.K9MailLib.DEBUG_PROTOCOL_IMAP; 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 f037aadd84..2230ea4c64 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 @@ -1,5 +1,6 @@ package com.fsck.k9.mail.store.imap +import com.fsck.k9.logging.Timber import com.fsck.k9.mail.Body import com.fsck.k9.mail.BodyFactory import com.fsck.k9.mail.FetchProfile @@ -24,7 +25,6 @@ import java.util.LinkedHashSet import java.util.Locale import kotlin.math.max import kotlin.math.min -import timber.log.Timber internal class RealImapFolder( private val internalImapStore: InternalImapStore, diff --git a/mail/protocols/imap/src/main/java/com/fsck/k9/mail/store/imap/RealImapFolderIdler.kt b/mail/protocols/imap/src/main/java/com/fsck/k9/mail/store/imap/RealImapFolderIdler.kt index c1c6d711a2..12b55d660e 100644 --- a/mail/protocols/imap/src/main/java/com/fsck/k9/mail/store/imap/RealImapFolderIdler.kt +++ b/mail/protocols/imap/src/main/java/com/fsck/k9/mail/store/imap/RealImapFolderIdler.kt @@ -1,9 +1,9 @@ package com.fsck.k9.mail.store.imap +import com.fsck.k9.logging.Timber import com.fsck.k9.mail.MessagingException import com.fsck.k9.mail.power.WakeLock import java.io.IOException -import timber.log.Timber private const val SOCKET_EXTRA_TIMEOUT_MS = 2 * 60 * 1000L diff --git a/mail/protocols/imap/src/main/java/com/fsck/k9/mail/store/imap/RealImapStore.java b/mail/protocols/imap/src/main/java/com/fsck/k9/mail/store/imap/RealImapStore.java index 1f89e0ce9c..98b4fb112f 100644 --- a/mail/protocols/imap/src/main/java/com/fsck/k9/mail/store/imap/RealImapStore.java +++ b/mail/protocols/imap/src/main/java/com/fsck/k9/mail/store/imap/RealImapStore.java @@ -13,6 +13,7 @@ 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.ConnectionSecurity; import com.fsck.k9.mail.Flag; @@ -23,7 +24,6 @@ import com.fsck.k9.mail.oauth.OAuth2TokenProvider; import com.fsck.k9.mail.ssl.TrustedSocketFactory; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -import timber.log.Timber; /** diff --git a/mail/protocols/pop3/build.gradle b/mail/protocols/pop3/build.gradle index a99cfe4f06..d041d7afbd 100644 --- a/mail/protocols/pop3/build.gradle +++ b/mail/protocols/pop3/build.gradle @@ -7,8 +7,6 @@ if (rootProject.testCoverage) { dependencies { api project(":mail:common") - implementation "com.jakewharton.timber:timber:${versions.timber}" - testImplementation project(":mail:testing") testImplementation "org.robolectric:robolectric:${versions.robolectric}" testImplementation "junit:junit:${versions.junit}" diff --git a/mail/protocols/pop3/src/main/java/com/fsck/k9/mail/store/pop3/Pop3Connection.java b/mail/protocols/pop3/src/main/java/com/fsck/k9/mail/store/pop3/Pop3Connection.java index 2a0d004e26..aaff29d73d 100644 --- a/mail/protocols/pop3/src/main/java/com/fsck/k9/mail/store/pop3/Pop3Connection.java +++ b/mail/protocols/pop3/src/main/java/com/fsck/k9/mail/store/pop3/Pop3Connection.java @@ -17,6 +17,7 @@ import java.util.Arrays; import java.util.List; import java.util.Locale; +import com.fsck.k9.logging.Timber; import com.fsck.k9.mail.AuthType; import com.fsck.k9.mail.Authentication; import com.fsck.k9.mail.AuthenticationFailedException; @@ -28,7 +29,6 @@ import com.fsck.k9.mail.filter.Base64; import com.fsck.k9.mail.filter.Hex; import com.fsck.k9.mail.ssl.TrustedSocketFactory; import javax.net.ssl.SSLException; -import timber.log.Timber; import static com.fsck.k9.mail.CertificateValidationException.Reason.MissingCapability; import static com.fsck.k9.mail.K9MailLib.DEBUG_PROTOCOL_POP3; diff --git a/mail/protocols/pop3/src/main/java/com/fsck/k9/mail/store/pop3/Pop3Folder.java b/mail/protocols/pop3/src/main/java/com/fsck/k9/mail/store/pop3/Pop3Folder.java index 870b9015aa..3271626f90 100644 --- a/mail/protocols/pop3/src/main/java/com/fsck/k9/mail/store/pop3/Pop3Folder.java +++ b/mail/protocols/pop3/src/main/java/com/fsck/k9/mail/store/pop3/Pop3Folder.java @@ -10,12 +10,12 @@ import java.util.Locale; import java.util.Map; import java.util.Set; +import com.fsck.k9.logging.Timber; import com.fsck.k9.mail.FetchProfile; import com.fsck.k9.mail.Flag; import com.fsck.k9.mail.K9MailLib; import com.fsck.k9.mail.MessageRetrievalListener; import com.fsck.k9.mail.MessagingException; -import timber.log.Timber; import static com.fsck.k9.mail.K9MailLib.DEBUG_PROTOCOL_POP3; import static com.fsck.k9.mail.store.pop3.Pop3Commands.*; diff --git a/mail/protocols/smtp/build.gradle b/mail/protocols/smtp/build.gradle index 8ec2ecb1d2..10a602852c 100644 --- a/mail/protocols/smtp/build.gradle +++ b/mail/protocols/smtp/build.gradle @@ -10,7 +10,6 @@ dependencies { implementation "commons-io:commons-io:${versions.commonsIo}" implementation "com.squareup.okio:okio:${versions.okio}" - implementation "com.jakewharton.timber:timber:${versions.timber}" testImplementation project(":mail:testing") testImplementation "org.robolectric:robolectric:${versions.robolectric}" diff --git a/mail/protocols/smtp/src/main/java/com/fsck/k9/mail/transport/smtp/SmtpTransport.kt b/mail/protocols/smtp/src/main/java/com/fsck/k9/mail/transport/smtp/SmtpTransport.kt index 2c7e1f191f..30fd795c5a 100644 --- a/mail/protocols/smtp/src/main/java/com/fsck/k9/mail/transport/smtp/SmtpTransport.kt +++ b/mail/protocols/smtp/src/main/java/com/fsck/k9/mail/transport/smtp/SmtpTransport.kt @@ -1,5 +1,6 @@ package com.fsck.k9.mail.transport.smtp +import com.fsck.k9.logging.Timber import com.fsck.k9.mail.Address import com.fsck.k9.mail.AuthType import com.fsck.k9.mail.Authentication @@ -36,7 +37,6 @@ import java.security.GeneralSecurityException import java.util.Locale import javax.net.ssl.SSLException import org.apache.commons.io.IOUtils -import timber.log.Timber private const val SMTP_CONTINUE_REQUEST = 334 private const val SMTP_AUTHENTICATION_FAILURE_ERROR_CODE = 535 diff --git a/mail/protocols/webdav/build.gradle b/mail/protocols/webdav/build.gradle index a14c8083cb..5efe157ac8 100644 --- a/mail/protocols/webdav/build.gradle +++ b/mail/protocols/webdav/build.gradle @@ -8,7 +8,6 @@ dependencies { api project(":mail:common") implementation "commons-io:commons-io:${versions.commonsIo}" - implementation "com.jakewharton.timber:timber:${versions.timber}" testImplementation project(":mail:testing") testImplementation "org.robolectric:robolectric:${versions.robolectric}" diff --git a/mail/protocols/webdav/src/main/java/com/fsck/k9/mail/store/webdav/DataSet.java b/mail/protocols/webdav/src/main/java/com/fsck/k9/mail/store/webdav/DataSet.java index 1ca8179ee3..c1d949fa14 100644 --- a/mail/protocols/webdav/src/main/java/com/fsck/k9/mail/store/webdav/DataSet.java +++ b/mail/protocols/webdav/src/main/java/com/fsck/k9/mail/store/webdav/DataSet.java @@ -10,7 +10,8 @@ import java.util.List; import java.util.Locale; import java.util.Map; -import timber.log.Timber; +import com.fsck.k9.logging.Timber; + /** * Maintains WebDAV data diff --git a/mail/protocols/webdav/src/main/java/com/fsck/k9/mail/store/webdav/HttpGeneric.java b/mail/protocols/webdav/src/main/java/com/fsck/k9/mail/store/webdav/HttpGeneric.java index 008b2aa432..09fa2f1652 100644 --- a/mail/protocols/webdav/src/main/java/com/fsck/k9/mail/store/webdav/HttpGeneric.java +++ b/mail/protocols/webdav/src/main/java/com/fsck/k9/mail/store/webdav/HttpGeneric.java @@ -1,9 +1,9 @@ package com.fsck.k9.mail.store.webdav; +import com.fsck.k9.logging.Timber; import com.fsck.k9.mail.K9MailLib; import org.apache.http.client.methods.HttpEntityEnclosingRequestBase; -import timber.log.Timber; import java.net.URI; @@ -80,4 +80,4 @@ public class HttpGeneric extends HttpEntityEnclosingRequestBase { METHOD_NAME = method; } } -} \ No newline at end of file +} diff --git a/mail/protocols/webdav/src/main/java/com/fsck/k9/mail/store/webdav/WebDavFolder.java b/mail/protocols/webdav/src/main/java/com/fsck/k9/mail/store/webdav/WebDavFolder.java index ddf5adf28d..6fde0f2fde 100644 --- a/mail/protocols/webdav/src/main/java/com/fsck/k9/mail/store/webdav/WebDavFolder.java +++ b/mail/protocols/webdav/src/main/java/com/fsck/k9/mail/store/webdav/WebDavFolder.java @@ -1,5 +1,6 @@ package com.fsck.k9.mail.store.webdav; +import com.fsck.k9.logging.Timber; import com.fsck.k9.mail.FetchProfile; import com.fsck.k9.mail.Flag; import com.fsck.k9.mail.FolderType; @@ -14,7 +15,6 @@ import org.apache.http.HttpEntity; import org.apache.http.HttpResponse; import org.apache.http.client.methods.HttpGet; import org.apache.http.entity.StringEntity; -import timber.log.Timber; import java.io.BufferedOutputStream; import java.io.BufferedReader; diff --git a/mail/protocols/webdav/src/main/java/com/fsck/k9/mail/store/webdav/WebDavHttpClient.java b/mail/protocols/webdav/src/main/java/com/fsck/k9/mail/store/webdav/WebDavHttpClient.java index 05bda8a81e..47b424ea43 100644 --- a/mail/protocols/webdav/src/main/java/com/fsck/k9/mail/store/webdav/WebDavHttpClient.java +++ b/mail/protocols/webdav/src/main/java/com/fsck/k9/mail/store/webdav/WebDavHttpClient.java @@ -1,5 +1,6 @@ package com.fsck.k9.mail.store.webdav; +import com.fsck.k9.logging.Timber; import org.apache.http.Header; import org.apache.http.HttpEntity; import org.apache.http.HttpRequest; @@ -7,7 +8,6 @@ import org.apache.http.HttpResponse; import org.apache.http.client.methods.HttpUriRequest; import org.apache.http.impl.client.DefaultHttpClient; import org.apache.http.protocol.HttpContext; -import timber.log.Timber; import java.io.IOException; import java.io.InputStream; diff --git a/mail/protocols/webdav/src/main/java/com/fsck/k9/mail/store/webdav/WebDavMessage.java b/mail/protocols/webdav/src/main/java/com/fsck/k9/mail/store/webdav/WebDavMessage.java index ffbbcbc0fb..42f4c5bf41 100644 --- a/mail/protocols/webdav/src/main/java/com/fsck/k9/mail/store/webdav/WebDavMessage.java +++ b/mail/protocols/webdav/src/main/java/com/fsck/k9/mail/store/webdav/WebDavMessage.java @@ -1,13 +1,12 @@ package com.fsck.k9.mail.store.webdav; +import com.fsck.k9.logging.Timber; import com.fsck.k9.mail.MessagingException; import com.fsck.k9.mail.internet.MimeMessage; import java.util.Locale; import java.util.Map; -import timber.log.Timber; - import static com.fsck.k9.mail.helper.UrlEncodingHelper.decodeUtf8; import static com.fsck.k9.mail.helper.UrlEncodingHelper.encodeUtf8; diff --git a/mail/protocols/webdav/src/main/java/com/fsck/k9/mail/store/webdav/WebDavStore.java b/mail/protocols/webdav/src/main/java/com/fsck/k9/mail/store/webdav/WebDavStore.java index c11282ac4c..7a6fdada95 100644 --- a/mail/protocols/webdav/src/main/java/com/fsck/k9/mail/store/webdav/WebDavStore.java +++ b/mail/protocols/webdav/src/main/java/com/fsck/k9/mail/store/webdav/WebDavStore.java @@ -16,6 +16,7 @@ import java.util.LinkedList; import java.util.List; import java.util.Map; +import com.fsck.k9.logging.Timber; import com.fsck.k9.mail.CertificateValidationException; import com.fsck.k9.mail.ConnectionSecurity; import com.fsck.k9.mail.FolderType; @@ -46,7 +47,6 @@ import org.apache.http.protocol.HttpContext; import org.xml.sax.InputSource; import org.xml.sax.SAXException; import org.xml.sax.XMLReader; -import timber.log.Timber; import static com.fsck.k9.mail.K9MailLib.DEBUG_PROTOCOL_WEBDAV; import static com.fsck.k9.mail.helper.UrlEncodingHelper.decodeUtf8; diff --git a/mail/protocols/webdav/src/main/java/com/fsck/k9/mail/transport/WebDavTransport.java b/mail/protocols/webdav/src/main/java/com/fsck/k9/mail/transport/WebDavTransport.java index d133b0e740..eddf97680d 100644 --- a/mail/protocols/webdav/src/main/java/com/fsck/k9/mail/transport/WebDavTransport.java +++ b/mail/protocols/webdav/src/main/java/com/fsck/k9/mail/transport/WebDavTransport.java @@ -3,6 +3,7 @@ package com.fsck.k9.mail.transport; import java.util.Collections; +import com.fsck.k9.logging.Timber; import com.fsck.k9.mail.K9MailLib; import com.fsck.k9.mail.Message; import com.fsck.k9.mail.MessagingException; @@ -12,7 +13,6 @@ import com.fsck.k9.mail.ssl.TrustManagerFactory; import com.fsck.k9.mail.store.webdav.DraftsFolderProvider; import com.fsck.k9.mail.store.webdav.SniHostSetter; import com.fsck.k9.mail.store.webdav.WebDavStore; -import timber.log.Timber; public class WebDavTransport extends Transport { private WebDavStore store; -- GitLab From ad337c0395cab1c85246abe93048a67febbc8d71 Mon Sep 17 00:00:00 2001 From: cketti Date: Mon, 2 May 2022 03:36:18 +0200 Subject: [PATCH 43/75] Move mime type utility functions to :k9mail:app:core --- .../java/com/fsck/k9/helper/MimeTypeUtil.java | 925 ++++++++++++++++++ .../fsck/k9/mailstore/AttachmentViewInfo.java | 8 +- .../extractors/AttachmentInfoExtractor.java | 3 +- .../extractors/BasicPartInfoExtractor.kt | 4 +- .../fsck/k9/provider/AttachmentProvider.java | 4 +- .../activity/loader/AttachmentInfoLoader.java | 5 +- .../com/fsck/k9/activity/misc/Attachment.java | 8 +- .../ui/messageview/AttachmentController.java | 8 +- .../fsck/k9/mail/internet/MimeUtility.java | 916 ----------------- .../java/com/fsck/k9/mail/MimeTypeTest.kt | 7 +- .../k9/mail/internet/MimeUtilityHelper.kt | 5 - 11 files changed, 948 insertions(+), 945 deletions(-) create mode 100644 app/core/src/main/java/com/fsck/k9/helper/MimeTypeUtil.java delete mode 100644 mail/common/src/test/java/com/fsck/k9/mail/internet/MimeUtilityHelper.kt diff --git a/app/core/src/main/java/com/fsck/k9/helper/MimeTypeUtil.java b/app/core/src/main/java/com/fsck/k9/helper/MimeTypeUtil.java new file mode 100644 index 0000000000..57266fa000 --- /dev/null +++ b/app/core/src/main/java/com/fsck/k9/helper/MimeTypeUtil.java @@ -0,0 +1,925 @@ +package com.fsck.k9.helper; + + +import java.util.Locale; + +import org.jetbrains.annotations.NotNull; + + +public class MimeTypeUtil { + public static final String DEFAULT_ATTACHMENT_MIME_TYPE = "application/octet-stream"; + public static final String K9_SETTINGS_MIME_TYPE = "application/x-k9settings"; + + /* + * http://www.w3schools.com/media/media_mimeref.asp + * + + * http://www.stdicon.com/mimetypes + */ + static final String[][] MIME_TYPE_BY_EXTENSION_MAP = new String[][] { + //* Do not delete the next three lines + { "", DEFAULT_ATTACHMENT_MIME_TYPE }, + { "k9s", K9_SETTINGS_MIME_TYPE }, + { "txt", "text/plain" }, + //* Do not delete the previous three lines + { "123", "application/vnd.lotus-1-2-3" }, + { "323", "text/h323" }, + { "3dml", "text/vnd.in3d.3dml" }, + { "3g2", "video/3gpp2" }, + { "3gp", "video/3gpp" }, + { "aab", "application/x-authorware-bin" }, + { "aac", "audio/x-aac" }, + { "aam", "application/x-authorware-map" }, + { "a", "application/octet-stream" }, + { "aas", "application/x-authorware-seg" }, + { "abw", "application/x-abiword" }, + { "acc", "application/vnd.americandynamics.acc" }, + { "ace", "application/x-ace-compressed" }, + { "acu", "application/vnd.acucobol" }, + { "acutc", "application/vnd.acucorp" }, + { "acx", "application/internet-property-stream" }, + { "adp", "audio/adpcm" }, + { "aep", "application/vnd.audiograph" }, + { "afm", "application/x-font-type1" }, + { "afp", "application/vnd.ibm.modcap" }, + { "ai", "application/postscript" }, + { "aif", "audio/x-aiff" }, + { "aifc", "audio/x-aiff" }, + { "aiff", "audio/x-aiff" }, + { "air", "application/vnd.adobe.air-application-installer-package+zip" }, + { "ami", "application/vnd.amiga.ami" }, + { "apk", "application/vnd.android.package-archive" }, + { "application", "application/x-ms-application" }, + { "apr", "application/vnd.lotus-approach" }, + { "asc", "application/pgp-signature" }, + { "asf", "video/x-ms-asf" }, + { "asm", "text/x-asm" }, + { "aso", "application/vnd.accpac.simply.aso" }, + { "asr", "video/x-ms-asf" }, + { "asx", "video/x-ms-asf" }, + { "atc", "application/vnd.acucorp" }, + { "atom", "application/atom+xml" }, + { "atomcat", "application/atomcat+xml" }, + { "atomsvc", "application/atomsvc+xml" }, + { "atx", "application/vnd.antix.game-component" }, + { "au", "audio/basic" }, + { "avi", "video/x-msvideo" }, + { "aw", "application/applixware" }, + { "axs", "application/olescript" }, + { "azf", "application/vnd.airzip.filesecure.azf" }, + { "azs", "application/vnd.airzip.filesecure.azs" }, + { "azw", "application/vnd.amazon.ebook" }, + { "bas", "text/plain" }, + { "bat", "application/x-msdownload" }, + { "bcpio", "application/x-bcpio" }, + { "bdf", "application/x-font-bdf" }, + { "bdm", "application/vnd.syncml.dm+wbxml" }, + { "bh2", "application/vnd.fujitsu.oasysprs" }, + { "bin", "application/octet-stream" }, + { "bmi", "application/vnd.bmi" }, + { "bmp", "image/bmp" }, + { "book", "application/vnd.framemaker" }, + { "box", "application/vnd.previewsystems.box" }, + { "boz", "application/x-bzip2" }, + { "bpk", "application/octet-stream" }, + { "btif", "image/prs.btif" }, + { "bz2", "application/x-bzip2" }, + { "bz", "application/x-bzip" }, + { "c4d", "application/vnd.clonk.c4group" }, + { "c4f", "application/vnd.clonk.c4group" }, + { "c4g", "application/vnd.clonk.c4group" }, + { "c4p", "application/vnd.clonk.c4group" }, + { "c4u", "application/vnd.clonk.c4group" }, + { "cab", "application/vnd.ms-cab-compressed" }, + { "car", "application/vnd.curl.car" }, + { "cat", "application/vnd.ms-pki.seccat" }, + { "cct", "application/x-director" }, + { "cc", "text/x-c" }, + { "ccxml", "application/ccxml+xml" }, + { "cdbcmsg", "application/vnd.contact.cmsg" }, + { "cdf", "application/x-cdf" }, + { "cdkey", "application/vnd.mediastation.cdkey" }, + { "cdx", "chemical/x-cdx" }, + { "cdxml", "application/vnd.chemdraw+xml" }, + { "cdy", "application/vnd.cinderella" }, + { "cer", "application/x-x509-ca-cert" }, + { "cgm", "image/cgm" }, + { "chat", "application/x-chat" }, + { "chm", "application/vnd.ms-htmlhelp" }, + { "chrt", "application/vnd.kde.kchart" }, + { "cif", "chemical/x-cif" }, + { "cii", "application/vnd.anser-web-certificate-issue-initiation" }, + { "cla", "application/vnd.claymore" }, + { "class", "application/java-vm" }, + { "clkk", "application/vnd.crick.clicker.keyboard" }, + { "clkp", "application/vnd.crick.clicker.palette" }, + { "clkt", "application/vnd.crick.clicker.template" }, + { "clkw", "application/vnd.crick.clicker.wordbank" }, + { "clkx", "application/vnd.crick.clicker" }, + { "clp", "application/x-msclip" }, + { "cmc", "application/vnd.cosmocaller" }, + { "cmdf", "chemical/x-cmdf" }, + { "cml", "chemical/x-cml" }, + { "cmp", "application/vnd.yellowriver-custom-menu" }, + { "cmx", "image/x-cmx" }, + { "cod", "application/vnd.rim.cod" }, + { "com", "application/x-msdownload" }, + { "conf", "text/plain" }, + { "cpio", "application/x-cpio" }, + { "cpp", "text/x-c" }, + { "cpt", "application/mac-compactpro" }, + { "crd", "application/x-mscardfile" }, + { "crl", "application/pkix-crl" }, + { "crt", "application/x-x509-ca-cert" }, + { "csh", "application/x-csh" }, + { "csml", "chemical/x-csml" }, + { "csp", "application/vnd.commonspace" }, + { "css", "text/css" }, + { "cst", "application/x-director" }, + { "csv", "text/csv" }, + { "c", "text/plain" }, + { "cu", "application/cu-seeme" }, + { "curl", "text/vnd.curl" }, + { "cww", "application/prs.cww" }, + { "cxt", "application/x-director" }, + { "cxx", "text/x-c" }, + { "daf", "application/vnd.mobius.daf" }, + { "dataless", "application/vnd.fdsn.seed" }, + { "davmount", "application/davmount+xml" }, + { "dcr", "application/x-director" }, + { "dcurl", "text/vnd.curl.dcurl" }, + { "dd2", "application/vnd.oma.dd2+xml" }, + { "ddd", "application/vnd.fujixerox.ddd" }, + { "deb", "application/x-debian-package" }, + { "def", "text/plain" }, + { "deploy", "application/octet-stream" }, + { "der", "application/x-x509-ca-cert" }, + { "dfac", "application/vnd.dreamfactory" }, + { "dic", "text/x-c" }, + { "diff", "text/plain" }, + { "dir", "application/x-director" }, + { "dis", "application/vnd.mobius.dis" }, + { "dist", "application/octet-stream" }, + { "distz", "application/octet-stream" }, + { "djv", "image/vnd.djvu" }, + { "djvu", "image/vnd.djvu" }, + { "dll", "application/x-msdownload" }, + { "dmg", "application/octet-stream" }, + { "dms", "application/octet-stream" }, + { "dna", "application/vnd.dna" }, + { "doc", "application/msword" }, + { "docm", "application/vnd.ms-word.document.macroenabled.12" }, + { "docx", "application/vnd.openxmlformats-officedocument.wordprocessingml.document" }, + { "dot", "application/msword" }, + { "dotm", "application/vnd.ms-word.template.macroenabled.12" }, + { "dotx", "application/vnd.openxmlformats-officedocument.wordprocessingml.template" }, + { "dp", "application/vnd.osgi.dp" }, + { "dpg", "application/vnd.dpgraph" }, + { "dsc", "text/prs.lines.tag" }, + { "dtb", "application/x-dtbook+xml" }, + { "dtd", "application/xml-dtd" }, + { "dts", "audio/vnd.dts" }, + { "dtshd", "audio/vnd.dts.hd" }, + { "dump", "application/octet-stream" }, + { "dvi", "application/x-dvi" }, + { "dwf", "model/vnd.dwf" }, + { "dwg", "image/vnd.dwg" }, + { "dxf", "image/vnd.dxf" }, + { "dxp", "application/vnd.spotfire.dxp" }, + { "dxr", "application/x-director" }, + { "ecelp4800", "audio/vnd.nuera.ecelp4800" }, + { "ecelp7470", "audio/vnd.nuera.ecelp7470" }, + { "ecelp9600", "audio/vnd.nuera.ecelp9600" }, + { "ecma", "application/ecmascript" }, + { "edm", "application/vnd.novadigm.edm" }, + { "edx", "application/vnd.novadigm.edx" }, + { "efif", "application/vnd.picsel" }, + { "ei6", "application/vnd.pg.osasli" }, + { "elc", "application/octet-stream" }, + { "eml", "message/rfc822" }, + { "emma", "application/emma+xml" }, + { "eol", "audio/vnd.digital-winds" }, + { "eot", "application/vnd.ms-fontobject" }, + { "eps", "application/postscript" }, + { "epub", "application/epub+zip" }, + { "es3", "application/vnd.eszigno3+xml" }, + { "esf", "application/vnd.epson.esf" }, + { "espass", "application/vnd.espass-espass+zip" }, + { "et3", "application/vnd.eszigno3+xml" }, + { "etx", "text/x-setext" }, + { "evy", "application/envoy" }, + { "exe", "application/octet-stream" }, + { "ext", "application/vnd.novadigm.ext" }, + { "ez2", "application/vnd.ezpix-album" }, + { "ez3", "application/vnd.ezpix-package" }, + { "ez", "application/andrew-inset" }, + { "f4v", "video/x-f4v" }, + { "f77", "text/x-fortran" }, + { "f90", "text/x-fortran" }, + { "fbs", "image/vnd.fastbidsheet" }, + { "fdf", "application/vnd.fdf" }, + { "fe_launch", "application/vnd.denovo.fcselayout-link" }, + { "fg5", "application/vnd.fujitsu.oasysgp" }, + { "fgd", "application/x-director" }, + { "fh4", "image/x-freehand" }, + { "fh5", "image/x-freehand" }, + { "fh7", "image/x-freehand" }, + { "fhc", "image/x-freehand" }, + { "fh", "image/x-freehand" }, + { "fif", "application/fractals" }, + { "fig", "application/x-xfig" }, + { "fli", "video/x-fli" }, + { "flo", "application/vnd.micrografx.flo" }, + { "flr", "x-world/x-vrml" }, + { "flv", "video/x-flv" }, + { "flw", "application/vnd.kde.kivio" }, + { "flx", "text/vnd.fmi.flexstor" }, + { "fly", "text/vnd.fly" }, + { "fm", "application/vnd.framemaker" }, + { "fnc", "application/vnd.frogans.fnc" }, + { "for", "text/x-fortran" }, + { "fpx", "image/vnd.fpx" }, + { "frame", "application/vnd.framemaker" }, + { "fsc", "application/vnd.fsc.weblaunch" }, + { "fst", "image/vnd.fst" }, + { "ftc", "application/vnd.fluxtime.clip" }, + { "f", "text/x-fortran" }, + { "fti", "application/vnd.anser-web-funds-transfer-initiation" }, + { "fvt", "video/vnd.fvt" }, + { "fzs", "application/vnd.fuzzysheet" }, + { "g3", "image/g3fax" }, + { "gac", "application/vnd.groove-account" }, + { "gdl", "model/vnd.gdl" }, + { "geo", "application/vnd.dynageo" }, + { "gex", "application/vnd.geometry-explorer" }, + { "ggb", "application/vnd.geogebra.file" }, + { "ggt", "application/vnd.geogebra.tool" }, + { "ghf", "application/vnd.groove-help" }, + { "gif", "image/gif" }, + { "gim", "application/vnd.groove-identity-message" }, + { "gmx", "application/vnd.gmx" }, + { "gnumeric", "application/x-gnumeric" }, + { "gph", "application/vnd.flographit" }, + { "gqf", "application/vnd.grafeq" }, + { "gqs", "application/vnd.grafeq" }, + { "gram", "application/srgs" }, + { "gre", "application/vnd.geometry-explorer" }, + { "grv", "application/vnd.groove-injector" }, + { "grxml", "application/srgs+xml" }, + { "gsf", "application/x-font-ghostscript" }, + { "gtar", "application/x-gtar" }, + { "gtm", "application/vnd.groove-tool-message" }, + { "gtw", "model/vnd.gtw" }, + { "gv", "text/vnd.graphviz" }, + { "gz", "application/x-gzip" }, + { "h261", "video/h261" }, + { "h263", "video/h263" }, + { "h264", "video/h264" }, + { "hbci", "application/vnd.hbci" }, + { "hdf", "application/x-hdf" }, + { "hh", "text/x-c" }, + { "hlp", "application/winhlp" }, + { "hpgl", "application/vnd.hp-hpgl" }, + { "hpid", "application/vnd.hp-hpid" }, + { "hps", "application/vnd.hp-hps" }, + { "hqx", "application/mac-binhex40" }, + { "hta", "application/hta" }, + { "htc", "text/x-component" }, + { "h", "text/plain" }, + { "htke", "application/vnd.kenameaapp" }, + { "html", "text/html" }, + { "htm", "text/html" }, + { "htt", "text/webviewhtml" }, + { "hvd", "application/vnd.yamaha.hv-dic" }, + { "hvp", "application/vnd.yamaha.hv-voice" }, + { "hvs", "application/vnd.yamaha.hv-script" }, + { "icc", "application/vnd.iccprofile" }, + { "ice", "x-conference/x-cooltalk" }, + { "icm", "application/vnd.iccprofile" }, + { "ico", "image/x-icon" }, + { "ics", "text/calendar" }, + { "ief", "image/ief" }, + { "ifb", "text/calendar" }, + { "ifm", "application/vnd.shana.informed.formdata" }, + { "iges", "model/iges" }, + { "igl", "application/vnd.igloader" }, + { "igs", "model/iges" }, + { "igx", "application/vnd.micrografx.igx" }, + { "iif", "application/vnd.shana.informed.interchange" }, + { "iii", "application/x-iphone" }, + { "imp", "application/vnd.accpac.simply.imp" }, + { "ims", "application/vnd.ms-ims" }, + { "ins", "application/x-internet-signup" }, + { "in", "text/plain" }, + { "ipk", "application/vnd.shana.informed.package" }, + { "irm", "application/vnd.ibm.rights-management" }, + { "irp", "application/vnd.irepository.package+xml" }, + { "iso", "application/octet-stream" }, + { "isp", "application/x-internet-signup" }, + { "itp", "application/vnd.shana.informed.formtemplate" }, + { "ivp", "application/vnd.immervision-ivp" }, + { "ivu", "application/vnd.immervision-ivu" }, + { "jad", "text/vnd.sun.j2me.app-descriptor" }, + { "jam", "application/vnd.jam" }, + { "jar", "application/java-archive" }, + { "java", "text/x-java-source" }, + { "jfif", "image/pipeg" }, + { "jisp", "application/vnd.jisp" }, + { "jlt", "application/vnd.hp-jlyt" }, + { "jnlp", "application/x-java-jnlp-file" }, + { "joda", "application/vnd.joost.joda-archive" }, + { "jpeg", "image/jpeg" }, + { "jpe", "image/jpeg" }, + { "jpg", "image/jpeg" }, + { "jpgm", "video/jpm" }, + { "jpgv", "video/jpeg" }, + { "jpm", "video/jpm" }, + { "js", "application/x-javascript" }, + { "json", "application/json" }, + { "kar", "audio/midi" }, + { "karbon", "application/vnd.kde.karbon" }, + { "kfo", "application/vnd.kde.kformula" }, + { "kia", "application/vnd.kidspiration" }, + { "kil", "application/x-killustrator" }, + { "kml", "application/vnd.google-earth.kml+xml" }, + { "kmz", "application/vnd.google-earth.kmz" }, + { "kne", "application/vnd.kinar" }, + { "knp", "application/vnd.kinar" }, + { "kon", "application/vnd.kde.kontour" }, + { "kpr", "application/vnd.kde.kpresenter" }, + { "kpt", "application/vnd.kde.kpresenter" }, + { "ksh", "text/plain" }, + { "ksp", "application/vnd.kde.kspread" }, + { "ktr", "application/vnd.kahootz" }, + { "ktz", "application/vnd.kahootz" }, + { "kwd", "application/vnd.kde.kword" }, + { "kwt", "application/vnd.kde.kword" }, + { "latex", "application/x-latex" }, + { "lbd", "application/vnd.llamagraphics.life-balance.desktop" }, + { "lbe", "application/vnd.llamagraphics.life-balance.exchange+xml" }, + { "les", "application/vnd.hhe.lesson-player" }, + { "lha", "application/octet-stream" }, + { "link66", "application/vnd.route66.link66+xml" }, + { "list3820", "application/vnd.ibm.modcap" }, + { "listafp", "application/vnd.ibm.modcap" }, + { "list", "text/plain" }, + { "log", "text/plain" }, + { "lostxml", "application/lost+xml" }, + { "lrf", "application/octet-stream" }, + { "lrm", "application/vnd.ms-lrm" }, + { "lsf", "video/x-la-asf" }, + { "lsx", "video/x-la-asf" }, + { "ltf", "application/vnd.frogans.ltf" }, + { "lvp", "audio/vnd.lucent.voice" }, + { "lwp", "application/vnd.lotus-wordpro" }, + { "lzh", "application/octet-stream" }, + { "m13", "application/x-msmediaview" }, + { "m14", "application/x-msmediaview" }, + { "m1v", "video/mpeg" }, + { "m2a", "audio/mpeg" }, + { "m2v", "video/mpeg" }, + { "m3a", "audio/mpeg" }, + { "m3u", "audio/x-mpegurl" }, + { "m4u", "video/vnd.mpegurl" }, + { "m4v", "video/x-m4v" }, + { "ma", "application/mathematica" }, + { "mag", "application/vnd.ecowin.chart" }, + { "maker", "application/vnd.framemaker" }, + { "man", "text/troff" }, + { "mathml", "application/mathml+xml" }, + { "mb", "application/mathematica" }, + { "mbk", "application/vnd.mobius.mbk" }, + { "mbox", "application/mbox" }, + { "mc1", "application/vnd.medcalcdata" }, + { "mcd", "application/vnd.mcd" }, + { "mcurl", "text/vnd.curl.mcurl" }, + { "mdb", "application/x-msaccess" }, + { "mdi", "image/vnd.ms-modi" }, + { "mesh", "model/mesh" }, + { "me", "text/troff" }, + { "mfm", "application/vnd.mfmp" }, + { "mgz", "application/vnd.proteus.magazine" }, + { "mht", "message/rfc822" }, + { "mhtml", "message/rfc822" }, + { "mid", "audio/midi" }, + { "midi", "audio/midi" }, + { "mif", "application/vnd.mif" }, + { "mime", "message/rfc822" }, + { "mj2", "video/mj2" }, + { "mjp2", "video/mj2" }, + { "mlp", "application/vnd.dolby.mlp" }, + { "mmd", "application/vnd.chipnuts.karaoke-mmd" }, + { "mmf", "application/vnd.smaf" }, + { "mmr", "image/vnd.fujixerox.edmics-mmr" }, + { "mny", "application/x-msmoney" }, + { "mobi", "application/x-mobipocket-ebook" }, + { "movie", "video/x-sgi-movie" }, + { "mov", "video/quicktime" }, + { "mp2a", "audio/mpeg" }, + { "mp2", "video/mpeg" }, + { "mp3", "audio/mpeg" }, + { "mp4a", "audio/mp4" }, + { "mp4s", "application/mp4" }, + { "mp4", "video/mp4" }, + { "mp4v", "video/mp4" }, + { "mpa", "video/mpeg" }, + { "mpc", "application/vnd.mophun.certificate" }, + { "mpeg", "video/mpeg" }, + { "mpe", "video/mpeg" }, + { "mpg4", "video/mp4" }, + { "mpga", "audio/mpeg" }, + { "mpg", "video/mpeg" }, + { "mpkg", "application/vnd.apple.installer+xml" }, + { "mpm", "application/vnd.blueice.multipass" }, + { "mpn", "application/vnd.mophun.application" }, + { "mpp", "application/vnd.ms-project" }, + { "mpt", "application/vnd.ms-project" }, + { "mpv2", "video/mpeg" }, + { "mpy", "application/vnd.ibm.minipay" }, + { "mqy", "application/vnd.mobius.mqy" }, + { "mrc", "application/marc" }, + { "mscml", "application/mediaservercontrol+xml" }, + { "mseed", "application/vnd.fdsn.mseed" }, + { "mseq", "application/vnd.mseq" }, + { "msf", "application/vnd.epson.msf" }, + { "msh", "model/mesh" }, + { "msi", "application/x-msdownload" }, + { "ms", "text/troff" }, + { "msty", "application/vnd.muvee.style" }, + { "mts", "model/vnd.mts" }, + { "mus", "application/vnd.musician" }, + { "musicxml", "application/vnd.recordare.musicxml+xml" }, + { "mvb", "application/x-msmediaview" }, + { "mxf", "application/mxf" }, + { "mxl", "application/vnd.recordare.musicxml" }, + { "mxml", "application/xv+xml" }, + { "mxs", "application/vnd.triscape.mxs" }, + { "mxu", "video/vnd.mpegurl" }, + { "nb", "application/mathematica" }, + { "nc", "application/x-netcdf" }, + { "ncx", "application/x-dtbncx+xml" }, + { "n-gage", "application/vnd.nokia.n-gage.symbian.install" }, + { "ngdat", "application/vnd.nokia.n-gage.data" }, + { "nlu", "application/vnd.neurolanguage.nlu" }, + { "nml", "application/vnd.enliven" }, + { "nnd", "application/vnd.noblenet-directory" }, + { "nns", "application/vnd.noblenet-sealer" }, + { "nnw", "application/vnd.noblenet-web" }, + { "npx", "image/vnd.net-fpx" }, + { "nsf", "application/vnd.lotus-notes" }, + { "nws", "message/rfc822" }, + { "oa2", "application/vnd.fujitsu.oasys2" }, + { "oa3", "application/vnd.fujitsu.oasys3" }, + { "o", "application/octet-stream" }, + { "oas", "application/vnd.fujitsu.oasys" }, + { "obd", "application/x-msbinder" }, + { "obj", "application/octet-stream" }, + { "oda", "application/oda" }, + { "odb", "application/vnd.oasis.opendocument.database" }, + { "odc", "application/vnd.oasis.opendocument.chart" }, + { "odf", "application/vnd.oasis.opendocument.formula" }, + { "odft", "application/vnd.oasis.opendocument.formula-template" }, + { "odg", "application/vnd.oasis.opendocument.graphics" }, + { "odi", "application/vnd.oasis.opendocument.image" }, + { "odp", "application/vnd.oasis.opendocument.presentation" }, + { "ods", "application/vnd.oasis.opendocument.spreadsheet" }, + { "odt", "application/vnd.oasis.opendocument.text" }, + { "oga", "audio/ogg" }, + { "ogg", "audio/ogg" }, + { "ogv", "video/ogg" }, + { "ogx", "application/ogg" }, + { "onepkg", "application/onenote" }, + { "onetmp", "application/onenote" }, + { "onetoc2", "application/onenote" }, + { "onetoc", "application/onenote" }, + { "opf", "application/oebps-package+xml" }, + { "oprc", "application/vnd.palm" }, + { "org", "application/vnd.lotus-organizer" }, + { "osf", "application/vnd.yamaha.openscoreformat" }, + { "osfpvg", "application/vnd.yamaha.openscoreformat.osfpvg+xml" }, + { "otc", "application/vnd.oasis.opendocument.chart-template" }, + { "otf", "application/x-font-otf" }, + { "otg", "application/vnd.oasis.opendocument.graphics-template" }, + { "oth", "application/vnd.oasis.opendocument.text-web" }, + { "oti", "application/vnd.oasis.opendocument.image-template" }, + { "otm", "application/vnd.oasis.opendocument.text-master" }, + { "otp", "application/vnd.oasis.opendocument.presentation-template" }, + { "ots", "application/vnd.oasis.opendocument.spreadsheet-template" }, + { "ott", "application/vnd.oasis.opendocument.text-template" }, + { "oxt", "application/vnd.openofficeorg.extension" }, + { "p10", "application/pkcs10" }, + { "p12", "application/x-pkcs12" }, + { "p7b", "application/x-pkcs7-certificates" }, + { "p7c", "application/x-pkcs7-mime" }, + { "p7m", "application/x-pkcs7-mime" }, + { "p7r", "application/x-pkcs7-certreqresp" }, + { "p7s", "application/x-pkcs7-signature" }, + { "pas", "text/x-pascal" }, + { "pbd", "application/vnd.powerbuilder6" }, + { "pbm", "image/x-portable-bitmap" }, + { "pcf", "application/x-font-pcf" }, + { "pcl", "application/vnd.hp-pcl" }, + { "pclxl", "application/vnd.hp-pclxl" }, + { "pct", "image/x-pict" }, + { "pcurl", "application/vnd.curl.pcurl" }, + { "pcx", "image/x-pcx" }, + { "pdb", "application/vnd.palm" }, + { "pdf", "application/pdf" }, + { "pfa", "application/x-font-type1" }, + { "pfb", "application/x-font-type1" }, + { "pfm", "application/x-font-type1" }, + { "pfr", "application/font-tdpfr" }, + { "pfx", "application/x-pkcs12" }, + { "pgm", "image/x-portable-graymap" }, + { "pgn", "application/x-chess-pgn" }, + { "pgp", "application/pgp-encrypted" }, + { "pic", "image/x-pict" }, + { "pkg", "application/octet-stream" }, + { "pki", "application/pkixcmp" }, + { "pkipath", "application/pkix-pkipath" }, + { "pkpass", "application/vnd-com.apple.pkpass" }, + { "pko", "application/ynd.ms-pkipko" }, + { "plb", "application/vnd.3gpp.pic-bw-large" }, + { "plc", "application/vnd.mobius.plc" }, + { "plf", "application/vnd.pocketlearn" }, + { "pls", "application/pls+xml" }, + { "pl", "text/plain" }, + { "pma", "application/x-perfmon" }, + { "pmc", "application/x-perfmon" }, + { "pml", "application/x-perfmon" }, + { "pmr", "application/x-perfmon" }, + { "pmw", "application/x-perfmon" }, + { "png", "image/png" }, + { "pnm", "image/x-portable-anymap" }, + { "portpkg", "application/vnd.macports.portpkg" }, + { "pot,", "application/vnd.ms-powerpoint" }, + { "pot", "application/vnd.ms-powerpoint" }, + { "potm", "application/vnd.ms-powerpoint.template.macroenabled.12" }, + { "potx", "application/vnd.openxmlformats-officedocument.presentationml.template" }, + { "ppa", "application/vnd.ms-powerpoint" }, + { "ppam", "application/vnd.ms-powerpoint.addin.macroenabled.12" }, + { "ppd", "application/vnd.cups-ppd" }, + { "ppm", "image/x-portable-pixmap" }, + { "pps", "application/vnd.ms-powerpoint" }, + { "ppsm", "application/vnd.ms-powerpoint.slideshow.macroenabled.12" }, + { "ppsx", "application/vnd.openxmlformats-officedocument.presentationml.slideshow" }, + { "ppt", "application/vnd.ms-powerpoint" }, + { "pptm", "application/vnd.ms-powerpoint.presentation.macroenabled.12" }, + { "pptx", "application/vnd.openxmlformats-officedocument.presentationml.presentation" }, + { "pqa", "application/vnd.palm" }, + { "prc", "application/x-mobipocket-ebook" }, + { "pre", "application/vnd.lotus-freelance" }, + { "prf", "application/pics-rules" }, + { "ps", "application/postscript" }, + { "psb", "application/vnd.3gpp.pic-bw-small" }, + { "psd", "image/vnd.adobe.photoshop" }, + { "psf", "application/x-font-linux-psf" }, + { "p", "text/x-pascal" }, + { "ptid", "application/vnd.pvi.ptid1" }, + { "pub", "application/x-mspublisher" }, + { "pvb", "application/vnd.3gpp.pic-bw-var" }, + { "pwn", "application/vnd.3m.post-it-notes" }, + { "pwz", "application/vnd.ms-powerpoint" }, + { "pya", "audio/vnd.ms-playready.media.pya" }, + { "pyc", "application/x-python-code" }, + { "pyo", "application/x-python-code" }, + { "py", "text/x-python" }, + { "pyv", "video/vnd.ms-playready.media.pyv" }, + { "qam", "application/vnd.epson.quickanime" }, + { "qbo", "application/vnd.intu.qbo" }, + { "qfx", "application/vnd.intu.qfx" }, + { "qps", "application/vnd.publishare-delta-tree" }, + { "qt", "video/quicktime" }, + { "qwd", "application/vnd.quark.quarkxpress" }, + { "qwt", "application/vnd.quark.quarkxpress" }, + { "qxb", "application/vnd.quark.quarkxpress" }, + { "qxd", "application/vnd.quark.quarkxpress" }, + { "qxl", "application/vnd.quark.quarkxpress" }, + { "qxt", "application/vnd.quark.quarkxpress" }, + { "ra", "audio/x-pn-realaudio" }, + { "ram", "audio/x-pn-realaudio" }, + { "rar", "application/x-rar-compressed" }, + { "ras", "image/x-cmu-raster" }, + { "rcprofile", "application/vnd.ipunplugged.rcprofile" }, + { "rdf", "application/rdf+xml" }, + { "rdz", "application/vnd.data-vision.rdz" }, + { "rep", "application/vnd.businessobjects" }, + { "res", "application/x-dtbresource+xml" }, + { "rgb", "image/x-rgb" }, + { "rif", "application/reginfo+xml" }, + { "rl", "application/resource-lists+xml" }, + { "rlc", "image/vnd.fujixerox.edmics-rlc" }, + { "rld", "application/resource-lists-diff+xml" }, + { "rm", "application/vnd.rn-realmedia" }, + { "rmi", "audio/midi" }, + { "rmp", "audio/x-pn-realaudio-plugin" }, + { "rms", "application/vnd.jcp.javame.midlet-rms" }, + { "rnc", "application/relax-ng-compact-syntax" }, + { "roff", "text/troff" }, + { "rpm", "application/x-rpm" }, + { "rpss", "application/vnd.nokia.radio-presets" }, + { "rpst", "application/vnd.nokia.radio-preset" }, + { "rq", "application/sparql-query" }, + { "rs", "application/rls-services+xml" }, + { "rsd", "application/rsd+xml" }, + { "rss", "application/rss+xml" }, + { "rtf", "application/rtf" }, + { "rtx", "text/richtext" }, + { "saf", "application/vnd.yamaha.smaf-audio" }, + { "sbml", "application/sbml+xml" }, + { "sc", "application/vnd.ibm.secure-container" }, + { "scd", "application/x-msschedule" }, + { "scm", "application/vnd.lotus-screencam" }, + { "scq", "application/scvp-cv-request" }, + { "scs", "application/scvp-cv-response" }, + { "sct", "text/scriptlet" }, + { "scurl", "text/vnd.curl.scurl" }, + { "sda", "application/vnd.stardivision.draw" }, + { "sdc", "application/vnd.stardivision.calc" }, + { "sdd", "application/vnd.stardivision.impress" }, + { "sdkd", "application/vnd.solent.sdkm+xml" }, + { "sdkm", "application/vnd.solent.sdkm+xml" }, + { "sdp", "application/sdp" }, + { "sdw", "application/vnd.stardivision.writer" }, + { "see", "application/vnd.seemail" }, + { "seed", "application/vnd.fdsn.seed" }, + { "sema", "application/vnd.sema" }, + { "semd", "application/vnd.semd" }, + { "semf", "application/vnd.semf" }, + { "ser", "application/java-serialized-object" }, + { "setpay", "application/set-payment-initiation" }, + { "setreg", "application/set-registration-initiation" }, + { "sfd-hdstx", "application/vnd.hydrostatix.sof-data" }, + { "sfs", "application/vnd.spotfire.sfs" }, + { "sgl", "application/vnd.stardivision.writer-global" }, + { "sgml", "text/sgml" }, + { "sgm", "text/sgml" }, + { "sh", "application/x-sh" }, + { "shar", "application/x-shar" }, + { "shf", "application/shf+xml" }, + { "sic", "application/vnd.wap.sic" }, + { "sig", "application/pgp-signature" }, + { "silo", "model/mesh" }, + { "sis", "application/vnd.symbian.install" }, + { "sisx", "application/vnd.symbian.install" }, + { "sit", "application/x-stuffit" }, + { "si", "text/vnd.wap.si" }, + { "sitx", "application/x-stuffitx" }, + { "skd", "application/vnd.koan" }, + { "skm", "application/vnd.koan" }, + { "skp", "application/vnd.koan" }, + { "skt", "application/vnd.koan" }, + { "slc", "application/vnd.wap.slc" }, + { "sldm", "application/vnd.ms-powerpoint.slide.macroenabled.12" }, + { "sldx", "application/vnd.openxmlformats-officedocument.presentationml.slide" }, + { "slt", "application/vnd.epson.salt" }, + { "sl", "text/vnd.wap.sl" }, + { "smf", "application/vnd.stardivision.math" }, + { "smi", "application/smil+xml" }, + { "smil", "application/smil+xml" }, + { "snd", "audio/basic" }, + { "snf", "application/x-font-snf" }, + { "so", "application/octet-stream" }, + { "spc", "application/x-pkcs7-certificates" }, + { "spf", "application/vnd.yamaha.smaf-phrase" }, + { "spl", "application/x-futuresplash" }, + { "spot", "text/vnd.in3d.spot" }, + { "spp", "application/scvp-vp-response" }, + { "spq", "application/scvp-vp-request" }, + { "spx", "audio/ogg" }, + { "src", "application/x-wais-source" }, + { "srx", "application/sparql-results+xml" }, + { "sse", "application/vnd.kodak-descriptor" }, + { "ssf", "application/vnd.epson.ssf" }, + { "ssml", "application/ssml+xml" }, + { "sst", "application/vnd.ms-pkicertstore" }, + { "stc", "application/vnd.sun.xml.calc.template" }, + { "std", "application/vnd.sun.xml.draw.template" }, + { "s", "text/x-asm" }, + { "stf", "application/vnd.wt.stf" }, + { "sti", "application/vnd.sun.xml.impress.template" }, + { "stk", "application/hyperstudio" }, + { "stl", "application/vnd.ms-pki.stl" }, + { "stm", "text/html" }, + { "str", "application/vnd.pg.format" }, + { "stw", "application/vnd.sun.xml.writer.template" }, + { "sus", "application/vnd.sus-calendar" }, + { "susp", "application/vnd.sus-calendar" }, + { "sv4cpio", "application/x-sv4cpio" }, + { "sv4crc", "application/x-sv4crc" }, + { "svd", "application/vnd.svd" }, + { "svg", "image/svg+xml" }, + { "svgz", "image/svg+xml" }, + { "swa", "application/x-director" }, + { "swf", "application/x-shockwave-flash" }, + { "swi", "application/vnd.arastra.swi" }, + { "sxc", "application/vnd.sun.xml.calc" }, + { "sxd", "application/vnd.sun.xml.draw" }, + { "sxg", "application/vnd.sun.xml.writer.global" }, + { "sxi", "application/vnd.sun.xml.impress" }, + { "sxm", "application/vnd.sun.xml.math" }, + { "sxw", "application/vnd.sun.xml.writer" }, + { "tao", "application/vnd.tao.intent-module-archive" }, + { "t", "application/x-troff" }, + { "tar", "application/x-tar" }, + { "tcap", "application/vnd.3gpp2.tcap" }, + { "tcl", "application/x-tcl" }, + { "teacher", "application/vnd.smart.teacher" }, + { "tex", "application/x-tex" }, + { "texi", "application/x-texinfo" }, + { "texinfo", "application/x-texinfo" }, + { "text", "text/plain" }, + { "tfm", "application/x-tex-tfm" }, + { "tgz", "application/x-gzip" }, + { "tiff", "image/tiff" }, + { "tif", "image/tiff" }, + { "tmo", "application/vnd.tmobile-livetv" }, + { "torrent", "application/x-bittorrent" }, + { "tpl", "application/vnd.groove-tool-template" }, + { "tpt", "application/vnd.trid.tpt" }, + { "tra", "application/vnd.trueapp" }, + { "trm", "application/x-msterminal" }, + { "tr", "text/troff" }, + { "tsv", "text/tab-separated-values" }, + { "ttc", "application/x-font-ttf" }, + { "ttf", "application/x-font-ttf" }, + { "twd", "application/vnd.simtech-mindmapper" }, + { "twds", "application/vnd.simtech-mindmapper" }, + { "txd", "application/vnd.genomatix.tuxedo" }, + { "txf", "application/vnd.mobius.txf" }, + { "txt", "text/plain" }, + { "u32", "application/x-authorware-bin" }, + { "udeb", "application/x-debian-package" }, + { "ufd", "application/vnd.ufdl" }, + { "ufdl", "application/vnd.ufdl" }, + { "uls", "text/iuls" }, + { "umj", "application/vnd.umajin" }, + { "unityweb", "application/vnd.unity" }, + { "uoml", "application/vnd.uoml+xml" }, + { "uris", "text/uri-list" }, + { "uri", "text/uri-list" }, + { "urls", "text/uri-list" }, + { "ustar", "application/x-ustar" }, + { "utz", "application/vnd.uiq.theme" }, + { "uu", "text/x-uuencode" }, + { "vcd", "application/x-cdlink" }, + { "vcf", "text/x-vcard" }, + { "vcg", "application/vnd.groove-vcard" }, + { "vcs", "text/x-vcalendar" }, + { "vcx", "application/vnd.vcx" }, + { "vis", "application/vnd.visionary" }, + { "viv", "video/vnd.vivo" }, + { "vor", "application/vnd.stardivision.writer" }, + { "vox", "application/x-authorware-bin" }, + { "vrml", "x-world/x-vrml" }, + { "vsd", "application/vnd.visio" }, + { "vsf", "application/vnd.vsf" }, + { "vss", "application/vnd.visio" }, + { "vst", "application/vnd.visio" }, + { "vsw", "application/vnd.visio" }, + { "vtu", "model/vnd.vtu" }, + { "vxml", "application/voicexml+xml" }, + { "w3d", "application/x-director" }, + { "wad", "application/x-doom" }, + { "wav", "audio/x-wav" }, + { "wax", "audio/x-ms-wax" }, + { "wbmp", "image/vnd.wap.wbmp" }, + { "wbs", "application/vnd.criticaltools.wbs+xml" }, + { "wbxml", "application/vnd.wap.wbxml" }, + { "wcm", "application/vnd.ms-works" }, + { "wdb", "application/vnd.ms-works" }, + { "wiz", "application/msword" }, + { "wks", "application/vnd.ms-works" }, + { "wma", "audio/x-ms-wma" }, + { "wmd", "application/x-ms-wmd" }, + { "wmf", "application/x-msmetafile" }, + { "wmlc", "application/vnd.wap.wmlc" }, + { "wmlsc", "application/vnd.wap.wmlscriptc" }, + { "wmls", "text/vnd.wap.wmlscript" }, + { "wml", "text/vnd.wap.wml" }, + { "wm", "video/x-ms-wm" }, + { "wmv", "video/x-ms-wmv" }, + { "wmx", "video/x-ms-wmx" }, + { "wmz", "application/x-ms-wmz" }, + { "wpd", "application/vnd.wordperfect" }, + { "wpl", "application/vnd.ms-wpl" }, + { "wps", "application/vnd.ms-works" }, + { "wqd", "application/vnd.wqd" }, + { "wri", "application/x-mswrite" }, + { "wrl", "x-world/x-vrml" }, + { "wrz", "x-world/x-vrml" }, + { "wsdl", "application/wsdl+xml" }, + { "wspolicy", "application/wspolicy+xml" }, + { "wtb", "application/vnd.webturbo" }, + { "wvx", "video/x-ms-wvx" }, + { "x32", "application/x-authorware-bin" }, + { "x3d", "application/vnd.hzn-3d-crossword" }, + { "xaf", "x-world/x-vrml" }, + { "xap", "application/x-silverlight-app" }, + { "xar", "application/vnd.xara" }, + { "xbap", "application/x-ms-xbap" }, + { "xbd", "application/vnd.fujixerox.docuworks.binder" }, + { "xbm", "image/x-xbitmap" }, + { "xdm", "application/vnd.syncml.dm+xml" }, + { "xdp", "application/vnd.adobe.xdp+xml" }, + { "xdw", "application/vnd.fujixerox.docuworks" }, + { "xenc", "application/xenc+xml" }, + { "xer", "application/patch-ops-error+xml" }, + { "xfdf", "application/vnd.adobe.xfdf" }, + { "xfdl", "application/vnd.xfdl" }, + { "xht", "application/xhtml+xml" }, + { "xhtml", "application/xhtml+xml" }, + { "xhvml", "application/xv+xml" }, + { "xif", "image/vnd.xiff" }, + { "xla", "application/vnd.ms-excel" }, + { "xlam", "application/vnd.ms-excel.addin.macroenabled.12" }, + { "xlb", "application/vnd.ms-excel" }, + { "xlc", "application/vnd.ms-excel" }, + { "xlm", "application/vnd.ms-excel" }, + { "xls", "application/vnd.ms-excel" }, + { "xlsb", "application/vnd.ms-excel.sheet.binary.macroenabled.12" }, + { "xlsm", "application/vnd.ms-excel.sheet.macroenabled.12" }, + { "xlsx", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" }, + { "xlt", "application/vnd.ms-excel" }, + { "xltm", "application/vnd.ms-excel.template.macroenabled.12" }, + { "xltx", "application/vnd.openxmlformats-officedocument.spreadsheetml.template" }, + { "xlw", "application/vnd.ms-excel" }, + { "xml", "application/xml" }, + { "xo", "application/vnd.olpc-sugar" }, + { "xof", "x-world/x-vrml" }, + { "xop", "application/xop+xml" }, + { "xpdl", "application/xml" }, + { "xpi", "application/x-xpinstall" }, + { "xpm", "image/x-xpixmap" }, + { "xpr", "application/vnd.is-xpr" }, + { "xps", "application/vnd.ms-xpsdocument" }, + { "xpw", "application/vnd.intercon.formnet" }, + { "xpx", "application/vnd.intercon.formnet" }, + { "xsl", "application/xml" }, + { "xslt", "application/xslt+xml" }, + { "xsm", "application/vnd.syncml+xml" }, + { "xspf", "application/xspf+xml" }, + { "xul", "application/vnd.mozilla.xul+xml" }, + { "xvm", "application/xv+xml" }, + { "xvml", "application/xv+xml" }, + { "xwd", "image/x-xwindowdump" }, + { "xyz", "chemical/x-xyz" }, + { "z", "application/x-compress" }, + { "zaz", "application/vnd.zzazz.deck+xml" }, + { "zip", "application/zip" }, + { "zir", "application/vnd.zul" }, + { "zirz", "application/vnd.zul" }, + { "zmm", "application/vnd.handheld-entertainment+xml" } + }; + + public static boolean isDefaultMimeType(String mimeType) { + return isSameMimeType(mimeType, DEFAULT_ATTACHMENT_MIME_TYPE); + } + + public static String getMimeTypeByExtension(String filename) { + String returnedType = null; + String extension = null; + + if (filename != null && filename.lastIndexOf('.') != -1) { + extension = filename.substring(filename.lastIndexOf('.') + 1).toLowerCase(Locale.US); + returnedType = android.webkit.MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension); + } + // If the MIME type set by the user's mailer is application/octet-stream, try to figure + // out whether there's a sane file type extension. + if (returnedType != null && !isSameMimeType(returnedType, DEFAULT_ATTACHMENT_MIME_TYPE)) { + return returnedType; + } else if (extension != null) { + for (String[] contentTypeMapEntry : MIME_TYPE_BY_EXTENSION_MAP) { + if (contentTypeMapEntry[0].equals(extension)) { + return contentTypeMapEntry[1]; + } + } + } + + return DEFAULT_ATTACHMENT_MIME_TYPE; + } + + public static String getExtensionByMimeType(@NotNull String mimeType) { + String lowerCaseMimeType = mimeType.toLowerCase(Locale.US); + for (String[] contentTypeMapEntry : MIME_TYPE_BY_EXTENSION_MAP) { + if (contentTypeMapEntry[1].equals(lowerCaseMimeType)) { + return contentTypeMapEntry[0]; + } + } + + return null; + } + + public static boolean isSupportedImageType(String mimeType) { + return isSameMimeType(mimeType, "image/jpeg") || isSameMimeType(mimeType, "image/png") || + isSameMimeType(mimeType, "image/gif") || isSameMimeType(mimeType, "image/webp"); + } + + public static boolean isSupportedImageExtension(String filename) { + String mimeType = getMimeTypeByExtension(filename); + return isSupportedImageType(mimeType); + } + + public static boolean isSameMimeType(String mimeType, String otherMimeType) { + return mimeType != null && mimeType.equalsIgnoreCase(otherMimeType); + } +} diff --git a/app/core/src/main/java/com/fsck/k9/mailstore/AttachmentViewInfo.java b/app/core/src/main/java/com/fsck/k9/mailstore/AttachmentViewInfo.java index a3403094b2..47a5fa8161 100644 --- a/app/core/src/main/java/com/fsck/k9/mailstore/AttachmentViewInfo.java +++ b/app/core/src/main/java/com/fsck/k9/mailstore/AttachmentViewInfo.java @@ -3,8 +3,8 @@ package com.fsck.k9.mailstore; import android.net.Uri; +import com.fsck.k9.helper.MimeTypeUtil; import com.fsck.k9.mail.Part; -import com.fsck.k9.mail.internet.MimeUtility; public class AttachmentViewInfo { @@ -48,8 +48,8 @@ public class AttachmentViewInfo { return false; } - return MimeUtility.isSupportedImageType(mimeType) || ( - MimeUtility.isSameMimeType(MimeUtility.DEFAULT_ATTACHMENT_MIME_TYPE, mimeType) && - MimeUtility.isSupportedImageExtension(displayName)); + return MimeTypeUtil.isSupportedImageType(mimeType) || ( + MimeTypeUtil.isSameMimeType(MimeTypeUtil.DEFAULT_ATTACHMENT_MIME_TYPE, mimeType) && + MimeTypeUtil.isSupportedImageExtension(displayName)); } } diff --git a/app/core/src/main/java/com/fsck/k9/message/extractors/AttachmentInfoExtractor.java b/app/core/src/main/java/com/fsck/k9/message/extractors/AttachmentInfoExtractor.java index 3f47997ab5..c570b5a256 100644 --- a/app/core/src/main/java/com/fsck/k9/message/extractors/AttachmentInfoExtractor.java +++ b/app/core/src/main/java/com/fsck/k9/message/extractors/AttachmentInfoExtractor.java @@ -12,6 +12,7 @@ import android.net.Uri; import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; +import com.fsck.k9.helper.MimeTypeUtil; import timber.log.Timber; import androidx.annotation.WorkerThread; @@ -120,7 +121,7 @@ public class AttachmentInfoExtractor { if (name == null) { String extension = null; if (mimeType != null) { - extension = MimeUtility.getExtensionByMimeType(mimeType); + extension = MimeTypeUtil.getExtensionByMimeType(mimeType); } name = "noname" + ((extension != null) ? "." + extension : ""); } diff --git a/app/core/src/main/java/com/fsck/k9/message/extractors/BasicPartInfoExtractor.kt b/app/core/src/main/java/com/fsck/k9/message/extractors/BasicPartInfoExtractor.kt index 6992a87a4a..9643a06f31 100644 --- a/app/core/src/main/java/com/fsck/k9/message/extractors/BasicPartInfoExtractor.kt +++ b/app/core/src/main/java/com/fsck/k9/message/extractors/BasicPartInfoExtractor.kt @@ -1,8 +1,8 @@ package com.fsck.k9.message.extractors +import com.fsck.k9.helper.MimeTypeUtil import com.fsck.k9.mail.Part import com.fsck.k9.mail.internet.MimeParameterDecoder -import com.fsck.k9.mail.internet.MimeUtility import com.fsck.k9.mail.internet.MimeValue private const val FALLBACK_NAME = "noname" @@ -31,7 +31,7 @@ class BasicPartInfoExtractor { } private fun String?.toDisplayName(): String { - val extension = this?.let { mimeType -> MimeUtility.getExtensionByMimeType(mimeType) } + val extension = this?.let { mimeType -> MimeTypeUtil.getExtensionByMimeType(mimeType) } return if (extension.isNullOrEmpty()) FALLBACK_NAME else "$FALLBACK_NAME.$extension" } diff --git a/app/core/src/main/java/com/fsck/k9/provider/AttachmentProvider.java b/app/core/src/main/java/com/fsck/k9/provider/AttachmentProvider.java index bee162ae0e..21a91725b7 100644 --- a/app/core/src/main/java/com/fsck/k9/provider/AttachmentProvider.java +++ b/app/core/src/main/java/com/fsck/k9/provider/AttachmentProvider.java @@ -15,13 +15,13 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.fsck.k9.DI; +import com.fsck.k9.helper.MimeTypeUtil; import com.fsck.k9.mailstore.LocalStoreProvider; import timber.log.Timber; import com.fsck.k9.Account; import com.fsck.k9.Preferences; import com.fsck.k9.mail.MessagingException; -import com.fsck.k9.mail.internet.MimeUtility; import com.fsck.k9.mailstore.LocalStore; import com.fsck.k9.mailstore.LocalStore.AttachmentInfo; import org.openintents.openpgp.util.OpenPgpApi.OpenPgpDataSource; @@ -156,7 +156,7 @@ public class AttachmentProvider extends ContentProvider { } } catch (MessagingException e) { Timber.e(e, "Unable to retrieve LocalStore for %s", account); - type = MimeUtility.DEFAULT_ATTACHMENT_MIME_TYPE; + type = MimeTypeUtil.DEFAULT_ATTACHMENT_MIME_TYPE; } return type; diff --git a/app/ui/legacy/src/main/java/com/fsck/k9/activity/loader/AttachmentInfoLoader.java b/app/ui/legacy/src/main/java/com/fsck/k9/activity/loader/AttachmentInfoLoader.java index 16019086e1..6750dd52b3 100644 --- a/app/ui/legacy/src/main/java/com/fsck/k9/activity/loader/AttachmentInfoLoader.java +++ b/app/ui/legacy/src/main/java/com/fsck/k9/activity/loader/AttachmentInfoLoader.java @@ -9,6 +9,7 @@ import android.net.Uri; import android.provider.OpenableColumns; import androidx.loader.content.AsyncTaskLoader; +import com.fsck.k9.helper.MimeTypeUtil; import com.fsck.k9.message.Attachment.LoadingState; import timber.log.Timber; @@ -81,11 +82,11 @@ public class AttachmentInfoLoader extends AsyncTaskLoader { } if (usableContentType == null) { - usableContentType = MimeUtility.getMimeTypeByExtension(name); + usableContentType = MimeTypeUtil.getMimeTypeByExtension(name); } if (!sourceAttachment.allowMessageType && MimeUtility.isMessageType(usableContentType)) { - usableContentType = MimeUtility.DEFAULT_ATTACHMENT_MIME_TYPE; + usableContentType = MimeTypeUtil.DEFAULT_ATTACHMENT_MIME_TYPE; } if (size <= 0) { diff --git a/app/ui/legacy/src/main/java/com/fsck/k9/activity/misc/Attachment.java b/app/ui/legacy/src/main/java/com/fsck/k9/activity/misc/Attachment.java index e9fbce7773..25a2b45d85 100644 --- a/app/ui/legacy/src/main/java/com/fsck/k9/activity/misc/Attachment.java +++ b/app/ui/legacy/src/main/java/com/fsck/k9/activity/misc/Attachment.java @@ -4,7 +4,7 @@ import android.net.Uri; import android.os.Parcel; import android.os.Parcelable; -import com.fsck.k9.mail.internet.MimeUtility; +import com.fsck.k9.helper.MimeTypeUtil; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -108,9 +108,9 @@ public class Attachment implements Parcelable, com.fsck.k9.message.Attachment { return false; } - return MimeUtility.isSupportedImageType(contentType) || ( - MimeUtility.isSameMimeType(MimeUtility.DEFAULT_ATTACHMENT_MIME_TYPE, contentType) && - MimeUtility.isSupportedImageExtension(filename)); + return MimeTypeUtil.isSupportedImageType(contentType) || ( + MimeTypeUtil.isSameMimeType(MimeTypeUtil.DEFAULT_ATTACHMENT_MIME_TYPE, contentType) && + MimeTypeUtil.isSupportedImageExtension(filename)); } private Attachment(Uri uri, LoadingState state, int loaderId, String contentType, boolean allowMessageType, diff --git a/app/ui/legacy/src/main/java/com/fsck/k9/ui/messageview/AttachmentController.java b/app/ui/legacy/src/main/java/com/fsck/k9/ui/messageview/AttachmentController.java index 0d90de27b9..fab62cbab3 100644 --- a/app/ui/legacy/src/main/java/com/fsck/k9/ui/messageview/AttachmentController.java +++ b/app/ui/legacy/src/main/java/com/fsck/k9/ui/messageview/AttachmentController.java @@ -21,9 +21,9 @@ import com.fsck.k9.Account; import com.fsck.k9.Preferences; import com.fsck.k9.controller.MessagingController; import com.fsck.k9.controller.SimpleMessagingListener; +import com.fsck.k9.helper.MimeTypeUtil; import com.fsck.k9.mail.Message; import com.fsck.k9.mail.Part; -import com.fsck.k9.mail.internet.MimeUtility; import com.fsck.k9.mailstore.AttachmentViewInfo; import com.fsck.k9.mailstore.LocalMessage; import com.fsck.k9.mailstore.LocalPart; @@ -140,11 +140,11 @@ public class AttachmentController { } String displayName = attachment.displayName; - String inferredMimeType = MimeUtility.getMimeTypeByExtension(displayName); + String inferredMimeType = MimeTypeUtil.getMimeTypeByExtension(displayName); IntentAndResolvedActivitiesCount resolvedIntentInfo; String mimeType = attachment.mimeType; - if (MimeUtility.isDefaultMimeType(mimeType)) { + if (MimeTypeUtil.isDefaultMimeType(mimeType)) { resolvedIntentInfo = getViewIntentForMimeType(intentDataUri, inferredMimeType); } else { resolvedIntentInfo = getViewIntentForMimeType(intentDataUri, mimeType); @@ -154,7 +154,7 @@ public class AttachmentController { } if (!resolvedIntentInfo.hasResolvedActivities()) { - resolvedIntentInfo = getViewIntentForMimeType(intentDataUri, MimeUtility.DEFAULT_ATTACHMENT_MIME_TYPE); + resolvedIntentInfo = getViewIntentForMimeType(intentDataUri, MimeTypeUtil.DEFAULT_ATTACHMENT_MIME_TYPE); } return resolvedIntentInfo.getIntent(); diff --git a/mail/common/src/main/java/com/fsck/k9/mail/internet/MimeUtility.java b/mail/common/src/main/java/com/fsck/k9/mail/internet/MimeUtility.java index dbe0ac785f..da138d2fa9 100644 --- a/mail/common/src/main/java/com/fsck/k9/mail/internet/MimeUtility.java +++ b/mail/common/src/main/java/com/fsck/k9/mail/internet/MimeUtility.java @@ -19,877 +19,9 @@ import com.fsck.k9.mail.Part; import org.apache.james.mime4j.codec.Base64InputStream; import org.apache.james.mime4j.codec.QuotedPrintableInputStream; import org.apache.james.mime4j.util.MimeUtil; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.VisibleForTesting; public class MimeUtility { - public static final String DEFAULT_ATTACHMENT_MIME_TYPE = "application/octet-stream"; - public static final String K9_SETTINGS_MIME_TYPE = "application/x-k9settings"; - - /* - * http://www.w3schools.com/media/media_mimeref.asp - * + - * http://www.stdicon.com/mimetypes - */ - @VisibleForTesting - static final String[][] MIME_TYPE_BY_EXTENSION_MAP = new String[][] { - //* Do not delete the next three lines - { "", DEFAULT_ATTACHMENT_MIME_TYPE }, - { "k9s", K9_SETTINGS_MIME_TYPE}, - { "txt", "text/plain"}, - //* Do not delete the previous three lines - { "123", "application/vnd.lotus-1-2-3"}, - { "323", "text/h323"}, - { "3dml", "text/vnd.in3d.3dml"}, - { "3g2", "video/3gpp2"}, - { "3gp", "video/3gpp"}, - { "aab", "application/x-authorware-bin"}, - { "aac", "audio/x-aac"}, - { "aam", "application/x-authorware-map"}, - { "a", "application/octet-stream"}, - { "aas", "application/x-authorware-seg"}, - { "abw", "application/x-abiword"}, - { "acc", "application/vnd.americandynamics.acc"}, - { "ace", "application/x-ace-compressed"}, - { "acu", "application/vnd.acucobol"}, - { "acutc", "application/vnd.acucorp"}, - { "acx", "application/internet-property-stream"}, - { "adp", "audio/adpcm"}, - { "aep", "application/vnd.audiograph"}, - { "afm", "application/x-font-type1"}, - { "afp", "application/vnd.ibm.modcap"}, - { "ai", "application/postscript"}, - { "aif", "audio/x-aiff"}, - { "aifc", "audio/x-aiff"}, - { "aiff", "audio/x-aiff"}, - { "air", "application/vnd.adobe.air-application-installer-package+zip"}, - { "ami", "application/vnd.amiga.ami"}, - { "apk", "application/vnd.android.package-archive"}, - { "application", "application/x-ms-application"}, - { "apr", "application/vnd.lotus-approach"}, - { "asc", "application/pgp-signature"}, - { "asf", "video/x-ms-asf"}, - { "asm", "text/x-asm"}, - { "aso", "application/vnd.accpac.simply.aso"}, - { "asr", "video/x-ms-asf"}, - { "asx", "video/x-ms-asf"}, - { "atc", "application/vnd.acucorp"}, - { "atom", "application/atom+xml"}, - { "atomcat", "application/atomcat+xml"}, - { "atomsvc", "application/atomsvc+xml"}, - { "atx", "application/vnd.antix.game-component"}, - { "au", "audio/basic"}, - { "avi", "video/x-msvideo"}, - { "aw", "application/applixware"}, - { "axs", "application/olescript"}, - { "azf", "application/vnd.airzip.filesecure.azf"}, - { "azs", "application/vnd.airzip.filesecure.azs"}, - { "azw", "application/vnd.amazon.ebook"}, - { "bas", "text/plain"}, - { "bat", "application/x-msdownload"}, - { "bcpio", "application/x-bcpio"}, - { "bdf", "application/x-font-bdf"}, - { "bdm", "application/vnd.syncml.dm+wbxml"}, - { "bh2", "application/vnd.fujitsu.oasysprs"}, - { "bin", "application/octet-stream"}, - { "bmi", "application/vnd.bmi"}, - { "bmp", "image/bmp"}, - { "book", "application/vnd.framemaker"}, - { "box", "application/vnd.previewsystems.box"}, - { "boz", "application/x-bzip2"}, - { "bpk", "application/octet-stream"}, - { "btif", "image/prs.btif"}, - { "bz2", "application/x-bzip2"}, - { "bz", "application/x-bzip"}, - { "c4d", "application/vnd.clonk.c4group"}, - { "c4f", "application/vnd.clonk.c4group"}, - { "c4g", "application/vnd.clonk.c4group"}, - { "c4p", "application/vnd.clonk.c4group"}, - { "c4u", "application/vnd.clonk.c4group"}, - { "cab", "application/vnd.ms-cab-compressed"}, - { "car", "application/vnd.curl.car"}, - { "cat", "application/vnd.ms-pki.seccat"}, - { "cct", "application/x-director"}, - { "cc", "text/x-c"}, - { "ccxml", "application/ccxml+xml"}, - { "cdbcmsg", "application/vnd.contact.cmsg"}, - { "cdf", "application/x-cdf"}, - { "cdkey", "application/vnd.mediastation.cdkey"}, - { "cdx", "chemical/x-cdx"}, - { "cdxml", "application/vnd.chemdraw+xml"}, - { "cdy", "application/vnd.cinderella"}, - { "cer", "application/x-x509-ca-cert"}, - { "cgm", "image/cgm"}, - { "chat", "application/x-chat"}, - { "chm", "application/vnd.ms-htmlhelp"}, - { "chrt", "application/vnd.kde.kchart"}, - { "cif", "chemical/x-cif"}, - { "cii", "application/vnd.anser-web-certificate-issue-initiation"}, - { "cla", "application/vnd.claymore"}, - { "class", "application/java-vm"}, - { "clkk", "application/vnd.crick.clicker.keyboard"}, - { "clkp", "application/vnd.crick.clicker.palette"}, - { "clkt", "application/vnd.crick.clicker.template"}, - { "clkw", "application/vnd.crick.clicker.wordbank"}, - { "clkx", "application/vnd.crick.clicker"}, - { "clp", "application/x-msclip"}, - { "cmc", "application/vnd.cosmocaller"}, - { "cmdf", "chemical/x-cmdf"}, - { "cml", "chemical/x-cml"}, - { "cmp", "application/vnd.yellowriver-custom-menu"}, - { "cmx", "image/x-cmx"}, - { "cod", "application/vnd.rim.cod"}, - { "com", "application/x-msdownload"}, - { "conf", "text/plain"}, - { "cpio", "application/x-cpio"}, - { "cpp", "text/x-c"}, - { "cpt", "application/mac-compactpro"}, - { "crd", "application/x-mscardfile"}, - { "crl", "application/pkix-crl"}, - { "crt", "application/x-x509-ca-cert"}, - { "csh", "application/x-csh"}, - { "csml", "chemical/x-csml"}, - { "csp", "application/vnd.commonspace"}, - { "css", "text/css"}, - { "cst", "application/x-director"}, - { "csv", "text/csv"}, - { "c", "text/plain"}, - { "cu", "application/cu-seeme"}, - { "curl", "text/vnd.curl"}, - { "cww", "application/prs.cww"}, - { "cxt", "application/x-director"}, - { "cxx", "text/x-c"}, - { "daf", "application/vnd.mobius.daf"}, - { "dataless", "application/vnd.fdsn.seed"}, - { "davmount", "application/davmount+xml"}, - { "dcr", "application/x-director"}, - { "dcurl", "text/vnd.curl.dcurl"}, - { "dd2", "application/vnd.oma.dd2+xml"}, - { "ddd", "application/vnd.fujixerox.ddd"}, - { "deb", "application/x-debian-package"}, - { "def", "text/plain"}, - { "deploy", "application/octet-stream"}, - { "der", "application/x-x509-ca-cert"}, - { "dfac", "application/vnd.dreamfactory"}, - { "dic", "text/x-c"}, - { "diff", "text/plain"}, - { "dir", "application/x-director"}, - { "dis", "application/vnd.mobius.dis"}, - { "dist", "application/octet-stream"}, - { "distz", "application/octet-stream"}, - { "djv", "image/vnd.djvu"}, - { "djvu", "image/vnd.djvu"}, - { "dll", "application/x-msdownload"}, - { "dmg", "application/octet-stream"}, - { "dms", "application/octet-stream"}, - { "dna", "application/vnd.dna"}, - { "doc", "application/msword"}, - { "docm", "application/vnd.ms-word.document.macroenabled.12"}, - { "docx", "application/vnd.openxmlformats-officedocument.wordprocessingml.document"}, - { "dot", "application/msword"}, - { "dotm", "application/vnd.ms-word.template.macroenabled.12"}, - { "dotx", "application/vnd.openxmlformats-officedocument.wordprocessingml.template"}, - { "dp", "application/vnd.osgi.dp"}, - { "dpg", "application/vnd.dpgraph"}, - { "dsc", "text/prs.lines.tag"}, - { "dtb", "application/x-dtbook+xml"}, - { "dtd", "application/xml-dtd"}, - { "dts", "audio/vnd.dts"}, - { "dtshd", "audio/vnd.dts.hd"}, - { "dump", "application/octet-stream"}, - { "dvi", "application/x-dvi"}, - { "dwf", "model/vnd.dwf"}, - { "dwg", "image/vnd.dwg"}, - { "dxf", "image/vnd.dxf"}, - { "dxp", "application/vnd.spotfire.dxp"}, - { "dxr", "application/x-director"}, - { "ecelp4800", "audio/vnd.nuera.ecelp4800"}, - { "ecelp7470", "audio/vnd.nuera.ecelp7470"}, - { "ecelp9600", "audio/vnd.nuera.ecelp9600"}, - { "ecma", "application/ecmascript"}, - { "edm", "application/vnd.novadigm.edm"}, - { "edx", "application/vnd.novadigm.edx"}, - { "efif", "application/vnd.picsel"}, - { "ei6", "application/vnd.pg.osasli"}, - { "elc", "application/octet-stream"}, - { "eml", "message/rfc822"}, - { "emma", "application/emma+xml"}, - { "eol", "audio/vnd.digital-winds"}, - { "eot", "application/vnd.ms-fontobject"}, - { "eps", "application/postscript"}, - { "epub", "application/epub+zip"}, - { "es3", "application/vnd.eszigno3+xml"}, - { "esf", "application/vnd.epson.esf"}, - { "espass", "application/vnd.espass-espass+zip"}, - { "et3", "application/vnd.eszigno3+xml"}, - { "etx", "text/x-setext"}, - { "evy", "application/envoy"}, - { "exe", "application/octet-stream"}, - { "ext", "application/vnd.novadigm.ext"}, - { "ez2", "application/vnd.ezpix-album"}, - { "ez3", "application/vnd.ezpix-package"}, - { "ez", "application/andrew-inset"}, - { "f4v", "video/x-f4v"}, - { "f77", "text/x-fortran"}, - { "f90", "text/x-fortran"}, - { "fbs", "image/vnd.fastbidsheet"}, - { "fdf", "application/vnd.fdf"}, - { "fe_launch", "application/vnd.denovo.fcselayout-link"}, - { "fg5", "application/vnd.fujitsu.oasysgp"}, - { "fgd", "application/x-director"}, - { "fh4", "image/x-freehand"}, - { "fh5", "image/x-freehand"}, - { "fh7", "image/x-freehand"}, - { "fhc", "image/x-freehand"}, - { "fh", "image/x-freehand"}, - { "fif", "application/fractals"}, - { "fig", "application/x-xfig"}, - { "fli", "video/x-fli"}, - { "flo", "application/vnd.micrografx.flo"}, - { "flr", "x-world/x-vrml"}, - { "flv", "video/x-flv"}, - { "flw", "application/vnd.kde.kivio"}, - { "flx", "text/vnd.fmi.flexstor"}, - { "fly", "text/vnd.fly"}, - { "fm", "application/vnd.framemaker"}, - { "fnc", "application/vnd.frogans.fnc"}, - { "for", "text/x-fortran"}, - { "fpx", "image/vnd.fpx"}, - { "frame", "application/vnd.framemaker"}, - { "fsc", "application/vnd.fsc.weblaunch"}, - { "fst", "image/vnd.fst"}, - { "ftc", "application/vnd.fluxtime.clip"}, - { "f", "text/x-fortran"}, - { "fti", "application/vnd.anser-web-funds-transfer-initiation"}, - { "fvt", "video/vnd.fvt"}, - { "fzs", "application/vnd.fuzzysheet"}, - { "g3", "image/g3fax"}, - { "gac", "application/vnd.groove-account"}, - { "gdl", "model/vnd.gdl"}, - { "geo", "application/vnd.dynageo"}, - { "gex", "application/vnd.geometry-explorer"}, - { "ggb", "application/vnd.geogebra.file"}, - { "ggt", "application/vnd.geogebra.tool"}, - { "ghf", "application/vnd.groove-help"}, - { "gif", "image/gif"}, - { "gim", "application/vnd.groove-identity-message"}, - { "gmx", "application/vnd.gmx"}, - { "gnumeric", "application/x-gnumeric"}, - { "gph", "application/vnd.flographit"}, - { "gqf", "application/vnd.grafeq"}, - { "gqs", "application/vnd.grafeq"}, - { "gram", "application/srgs"}, - { "gre", "application/vnd.geometry-explorer"}, - { "grv", "application/vnd.groove-injector"}, - { "grxml", "application/srgs+xml"}, - { "gsf", "application/x-font-ghostscript"}, - { "gtar", "application/x-gtar"}, - { "gtm", "application/vnd.groove-tool-message"}, - { "gtw", "model/vnd.gtw"}, - { "gv", "text/vnd.graphviz"}, - { "gz", "application/x-gzip"}, - { "h261", "video/h261"}, - { "h263", "video/h263"}, - { "h264", "video/h264"}, - { "hbci", "application/vnd.hbci"}, - { "hdf", "application/x-hdf"}, - { "hh", "text/x-c"}, - { "hlp", "application/winhlp"}, - { "hpgl", "application/vnd.hp-hpgl"}, - { "hpid", "application/vnd.hp-hpid"}, - { "hps", "application/vnd.hp-hps"}, - { "hqx", "application/mac-binhex40"}, - { "hta", "application/hta"}, - { "htc", "text/x-component"}, - { "h", "text/plain"}, - { "htke", "application/vnd.kenameaapp"}, - { "html", "text/html"}, - { "htm", "text/html"}, - { "htt", "text/webviewhtml"}, - { "hvd", "application/vnd.yamaha.hv-dic"}, - { "hvp", "application/vnd.yamaha.hv-voice"}, - { "hvs", "application/vnd.yamaha.hv-script"}, - { "icc", "application/vnd.iccprofile"}, - { "ice", "x-conference/x-cooltalk"}, - { "icm", "application/vnd.iccprofile"}, - { "ico", "image/x-icon"}, - { "ics", "text/calendar"}, - { "ief", "image/ief"}, - { "ifb", "text/calendar"}, - { "ifm", "application/vnd.shana.informed.formdata"}, - { "iges", "model/iges"}, - { "igl", "application/vnd.igloader"}, - { "igs", "model/iges"}, - { "igx", "application/vnd.micrografx.igx"}, - { "iif", "application/vnd.shana.informed.interchange"}, - { "iii", "application/x-iphone"}, - { "imp", "application/vnd.accpac.simply.imp"}, - { "ims", "application/vnd.ms-ims"}, - { "ins", "application/x-internet-signup"}, - { "in", "text/plain"}, - { "ipk", "application/vnd.shana.informed.package"}, - { "irm", "application/vnd.ibm.rights-management"}, - { "irp", "application/vnd.irepository.package+xml"}, - { "iso", "application/octet-stream"}, - { "isp", "application/x-internet-signup"}, - { "itp", "application/vnd.shana.informed.formtemplate"}, - { "ivp", "application/vnd.immervision-ivp"}, - { "ivu", "application/vnd.immervision-ivu"}, - { "jad", "text/vnd.sun.j2me.app-descriptor"}, - { "jam", "application/vnd.jam"}, - { "jar", "application/java-archive"}, - { "java", "text/x-java-source"}, - { "jfif", "image/pipeg"}, - { "jisp", "application/vnd.jisp"}, - { "jlt", "application/vnd.hp-jlyt"}, - { "jnlp", "application/x-java-jnlp-file"}, - { "joda", "application/vnd.joost.joda-archive"}, - { "jpeg", "image/jpeg"}, - { "jpe", "image/jpeg"}, - { "jpg", "image/jpeg"}, - { "jpgm", "video/jpm"}, - { "jpgv", "video/jpeg"}, - { "jpm", "video/jpm"}, - { "js", "application/x-javascript"}, - { "json", "application/json"}, - { "kar", "audio/midi"}, - { "karbon", "application/vnd.kde.karbon"}, - { "kfo", "application/vnd.kde.kformula"}, - { "kia", "application/vnd.kidspiration"}, - { "kil", "application/x-killustrator"}, - { "kml", "application/vnd.google-earth.kml+xml"}, - { "kmz", "application/vnd.google-earth.kmz"}, - { "kne", "application/vnd.kinar"}, - { "knp", "application/vnd.kinar"}, - { "kon", "application/vnd.kde.kontour"}, - { "kpr", "application/vnd.kde.kpresenter"}, - { "kpt", "application/vnd.kde.kpresenter"}, - { "ksh", "text/plain"}, - { "ksp", "application/vnd.kde.kspread"}, - { "ktr", "application/vnd.kahootz"}, - { "ktz", "application/vnd.kahootz"}, - { "kwd", "application/vnd.kde.kword"}, - { "kwt", "application/vnd.kde.kword"}, - { "latex", "application/x-latex"}, - { "lbd", "application/vnd.llamagraphics.life-balance.desktop"}, - { "lbe", "application/vnd.llamagraphics.life-balance.exchange+xml"}, - { "les", "application/vnd.hhe.lesson-player"}, - { "lha", "application/octet-stream"}, - { "link66", "application/vnd.route66.link66+xml"}, - { "list3820", "application/vnd.ibm.modcap"}, - { "listafp", "application/vnd.ibm.modcap"}, - { "list", "text/plain"}, - { "log", "text/plain"}, - { "lostxml", "application/lost+xml"}, - { "lrf", "application/octet-stream"}, - { "lrm", "application/vnd.ms-lrm"}, - { "lsf", "video/x-la-asf"}, - { "lsx", "video/x-la-asf"}, - { "ltf", "application/vnd.frogans.ltf"}, - { "lvp", "audio/vnd.lucent.voice"}, - { "lwp", "application/vnd.lotus-wordpro"}, - { "lzh", "application/octet-stream"}, - { "m13", "application/x-msmediaview"}, - { "m14", "application/x-msmediaview"}, - { "m1v", "video/mpeg"}, - { "m2a", "audio/mpeg"}, - { "m2v", "video/mpeg"}, - { "m3a", "audio/mpeg"}, - { "m3u", "audio/x-mpegurl"}, - { "m4u", "video/vnd.mpegurl"}, - { "m4v", "video/x-m4v"}, - { "ma", "application/mathematica"}, - { "mag", "application/vnd.ecowin.chart"}, - { "maker", "application/vnd.framemaker"}, - { "man", "text/troff"}, - { "mathml", "application/mathml+xml"}, - { "mb", "application/mathematica"}, - { "mbk", "application/vnd.mobius.mbk"}, - { "mbox", "application/mbox"}, - { "mc1", "application/vnd.medcalcdata"}, - { "mcd", "application/vnd.mcd"}, - { "mcurl", "text/vnd.curl.mcurl"}, - { "mdb", "application/x-msaccess"}, - { "mdi", "image/vnd.ms-modi"}, - { "mesh", "model/mesh"}, - { "me", "text/troff"}, - { "mfm", "application/vnd.mfmp"}, - { "mgz", "application/vnd.proteus.magazine"}, - { "mht", "message/rfc822"}, - { "mhtml", "message/rfc822"}, - { "mid", "audio/midi"}, - { "midi", "audio/midi"}, - { "mif", "application/vnd.mif"}, - { "mime", "message/rfc822"}, - { "mj2", "video/mj2"}, - { "mjp2", "video/mj2"}, - { "mlp", "application/vnd.dolby.mlp"}, - { "mmd", "application/vnd.chipnuts.karaoke-mmd"}, - { "mmf", "application/vnd.smaf"}, - { "mmr", "image/vnd.fujixerox.edmics-mmr"}, - { "mny", "application/x-msmoney"}, - { "mobi", "application/x-mobipocket-ebook"}, - { "movie", "video/x-sgi-movie"}, - { "mov", "video/quicktime"}, - { "mp2a", "audio/mpeg"}, - { "mp2", "video/mpeg"}, - { "mp3", "audio/mpeg"}, - { "mp4a", "audio/mp4"}, - { "mp4s", "application/mp4"}, - { "mp4", "video/mp4"}, - { "mp4v", "video/mp4"}, - { "mpa", "video/mpeg"}, - { "mpc", "application/vnd.mophun.certificate"}, - { "mpeg", "video/mpeg"}, - { "mpe", "video/mpeg"}, - { "mpg4", "video/mp4"}, - { "mpga", "audio/mpeg"}, - { "mpg", "video/mpeg"}, - { "mpkg", "application/vnd.apple.installer+xml"}, - { "mpm", "application/vnd.blueice.multipass"}, - { "mpn", "application/vnd.mophun.application"}, - { "mpp", "application/vnd.ms-project"}, - { "mpt", "application/vnd.ms-project"}, - { "mpv2", "video/mpeg"}, - { "mpy", "application/vnd.ibm.minipay"}, - { "mqy", "application/vnd.mobius.mqy"}, - { "mrc", "application/marc"}, - { "mscml", "application/mediaservercontrol+xml"}, - { "mseed", "application/vnd.fdsn.mseed"}, - { "mseq", "application/vnd.mseq"}, - { "msf", "application/vnd.epson.msf"}, - { "msh", "model/mesh"}, - { "msi", "application/x-msdownload"}, - { "ms", "text/troff"}, - { "msty", "application/vnd.muvee.style"}, - { "mts", "model/vnd.mts"}, - { "mus", "application/vnd.musician"}, - { "musicxml", "application/vnd.recordare.musicxml+xml"}, - { "mvb", "application/x-msmediaview"}, - { "mxf", "application/mxf"}, - { "mxl", "application/vnd.recordare.musicxml"}, - { "mxml", "application/xv+xml"}, - { "mxs", "application/vnd.triscape.mxs"}, - { "mxu", "video/vnd.mpegurl"}, - { "nb", "application/mathematica"}, - { "nc", "application/x-netcdf"}, - { "ncx", "application/x-dtbncx+xml"}, - { "n-gage", "application/vnd.nokia.n-gage.symbian.install"}, - { "ngdat", "application/vnd.nokia.n-gage.data"}, - { "nlu", "application/vnd.neurolanguage.nlu"}, - { "nml", "application/vnd.enliven"}, - { "nnd", "application/vnd.noblenet-directory"}, - { "nns", "application/vnd.noblenet-sealer"}, - { "nnw", "application/vnd.noblenet-web"}, - { "npx", "image/vnd.net-fpx"}, - { "nsf", "application/vnd.lotus-notes"}, - { "nws", "message/rfc822"}, - { "oa2", "application/vnd.fujitsu.oasys2"}, - { "oa3", "application/vnd.fujitsu.oasys3"}, - { "o", "application/octet-stream"}, - { "oas", "application/vnd.fujitsu.oasys"}, - { "obd", "application/x-msbinder"}, - { "obj", "application/octet-stream"}, - { "oda", "application/oda"}, - { "odb", "application/vnd.oasis.opendocument.database"}, - { "odc", "application/vnd.oasis.opendocument.chart"}, - { "odf", "application/vnd.oasis.opendocument.formula"}, - { "odft", "application/vnd.oasis.opendocument.formula-template"}, - { "odg", "application/vnd.oasis.opendocument.graphics"}, - { "odi", "application/vnd.oasis.opendocument.image"}, - { "odp", "application/vnd.oasis.opendocument.presentation"}, - { "ods", "application/vnd.oasis.opendocument.spreadsheet"}, - { "odt", "application/vnd.oasis.opendocument.text"}, - { "oga", "audio/ogg"}, - { "ogg", "audio/ogg"}, - { "ogv", "video/ogg"}, - { "ogx", "application/ogg"}, - { "onepkg", "application/onenote"}, - { "onetmp", "application/onenote"}, - { "onetoc2", "application/onenote"}, - { "onetoc", "application/onenote"}, - { "opf", "application/oebps-package+xml"}, - { "oprc", "application/vnd.palm"}, - { "org", "application/vnd.lotus-organizer"}, - { "osf", "application/vnd.yamaha.openscoreformat"}, - { "osfpvg", "application/vnd.yamaha.openscoreformat.osfpvg+xml"}, - { "otc", "application/vnd.oasis.opendocument.chart-template"}, - { "otf", "application/x-font-otf"}, - { "otg", "application/vnd.oasis.opendocument.graphics-template"}, - { "oth", "application/vnd.oasis.opendocument.text-web"}, - { "oti", "application/vnd.oasis.opendocument.image-template"}, - { "otm", "application/vnd.oasis.opendocument.text-master"}, - { "otp", "application/vnd.oasis.opendocument.presentation-template"}, - { "ots", "application/vnd.oasis.opendocument.spreadsheet-template"}, - { "ott", "application/vnd.oasis.opendocument.text-template"}, - { "oxt", "application/vnd.openofficeorg.extension"}, - { "p10", "application/pkcs10"}, - { "p12", "application/x-pkcs12"}, - { "p7b", "application/x-pkcs7-certificates"}, - { "p7c", "application/x-pkcs7-mime"}, - { "p7m", "application/x-pkcs7-mime"}, - { "p7r", "application/x-pkcs7-certreqresp"}, - { "p7s", "application/x-pkcs7-signature"}, - { "pas", "text/x-pascal"}, - { "pbd", "application/vnd.powerbuilder6"}, - { "pbm", "image/x-portable-bitmap"}, - { "pcf", "application/x-font-pcf"}, - { "pcl", "application/vnd.hp-pcl"}, - { "pclxl", "application/vnd.hp-pclxl"}, - { "pct", "image/x-pict"}, - { "pcurl", "application/vnd.curl.pcurl"}, - { "pcx", "image/x-pcx"}, - { "pdb", "application/vnd.palm"}, - { "pdf", "application/pdf"}, - { "pfa", "application/x-font-type1"}, - { "pfb", "application/x-font-type1"}, - { "pfm", "application/x-font-type1"}, - { "pfr", "application/font-tdpfr"}, - { "pfx", "application/x-pkcs12"}, - { "pgm", "image/x-portable-graymap"}, - { "pgn", "application/x-chess-pgn"}, - { "pgp", "application/pgp-encrypted"}, - { "pic", "image/x-pict"}, - { "pkg", "application/octet-stream"}, - { "pki", "application/pkixcmp"}, - { "pkipath", "application/pkix-pkipath"}, - { "pkpass", "application/vnd-com.apple.pkpass"}, - { "pko", "application/ynd.ms-pkipko"}, - { "plb", "application/vnd.3gpp.pic-bw-large"}, - { "plc", "application/vnd.mobius.plc"}, - { "plf", "application/vnd.pocketlearn"}, - { "pls", "application/pls+xml"}, - { "pl", "text/plain"}, - { "pma", "application/x-perfmon"}, - { "pmc", "application/x-perfmon"}, - { "pml", "application/x-perfmon"}, - { "pmr", "application/x-perfmon"}, - { "pmw", "application/x-perfmon"}, - { "png", "image/png"}, - { "pnm", "image/x-portable-anymap"}, - { "portpkg", "application/vnd.macports.portpkg"}, - { "pot,", "application/vnd.ms-powerpoint"}, - { "pot", "application/vnd.ms-powerpoint"}, - { "potm", "application/vnd.ms-powerpoint.template.macroenabled.12"}, - { "potx", "application/vnd.openxmlformats-officedocument.presentationml.template"}, - { "ppa", "application/vnd.ms-powerpoint"}, - { "ppam", "application/vnd.ms-powerpoint.addin.macroenabled.12"}, - { "ppd", "application/vnd.cups-ppd"}, - { "ppm", "image/x-portable-pixmap"}, - { "pps", "application/vnd.ms-powerpoint"}, - { "ppsm", "application/vnd.ms-powerpoint.slideshow.macroenabled.12"}, - { "ppsx", "application/vnd.openxmlformats-officedocument.presentationml.slideshow"}, - { "ppt", "application/vnd.ms-powerpoint"}, - { "pptm", "application/vnd.ms-powerpoint.presentation.macroenabled.12"}, - { "pptx", "application/vnd.openxmlformats-officedocument.presentationml.presentation"}, - { "pqa", "application/vnd.palm"}, - { "prc", "application/x-mobipocket-ebook"}, - { "pre", "application/vnd.lotus-freelance"}, - { "prf", "application/pics-rules"}, - { "ps", "application/postscript"}, - { "psb", "application/vnd.3gpp.pic-bw-small"}, - { "psd", "image/vnd.adobe.photoshop"}, - { "psf", "application/x-font-linux-psf"}, - { "p", "text/x-pascal"}, - { "ptid", "application/vnd.pvi.ptid1"}, - { "pub", "application/x-mspublisher"}, - { "pvb", "application/vnd.3gpp.pic-bw-var"}, - { "pwn", "application/vnd.3m.post-it-notes"}, - { "pwz", "application/vnd.ms-powerpoint"}, - { "pya", "audio/vnd.ms-playready.media.pya"}, - { "pyc", "application/x-python-code"}, - { "pyo", "application/x-python-code"}, - { "py", "text/x-python"}, - { "pyv", "video/vnd.ms-playready.media.pyv"}, - { "qam", "application/vnd.epson.quickanime"}, - { "qbo", "application/vnd.intu.qbo"}, - { "qfx", "application/vnd.intu.qfx"}, - { "qps", "application/vnd.publishare-delta-tree"}, - { "qt", "video/quicktime"}, - { "qwd", "application/vnd.quark.quarkxpress"}, - { "qwt", "application/vnd.quark.quarkxpress"}, - { "qxb", "application/vnd.quark.quarkxpress"}, - { "qxd", "application/vnd.quark.quarkxpress"}, - { "qxl", "application/vnd.quark.quarkxpress"}, - { "qxt", "application/vnd.quark.quarkxpress"}, - { "ra", "audio/x-pn-realaudio"}, - { "ram", "audio/x-pn-realaudio"}, - { "rar", "application/x-rar-compressed"}, - { "ras", "image/x-cmu-raster"}, - { "rcprofile", "application/vnd.ipunplugged.rcprofile"}, - { "rdf", "application/rdf+xml"}, - { "rdz", "application/vnd.data-vision.rdz"}, - { "rep", "application/vnd.businessobjects"}, - { "res", "application/x-dtbresource+xml"}, - { "rgb", "image/x-rgb"}, - { "rif", "application/reginfo+xml"}, - { "rl", "application/resource-lists+xml"}, - { "rlc", "image/vnd.fujixerox.edmics-rlc"}, - { "rld", "application/resource-lists-diff+xml"}, - { "rm", "application/vnd.rn-realmedia"}, - { "rmi", "audio/midi"}, - { "rmp", "audio/x-pn-realaudio-plugin"}, - { "rms", "application/vnd.jcp.javame.midlet-rms"}, - { "rnc", "application/relax-ng-compact-syntax"}, - { "roff", "text/troff"}, - { "rpm", "application/x-rpm"}, - { "rpss", "application/vnd.nokia.radio-presets"}, - { "rpst", "application/vnd.nokia.radio-preset"}, - { "rq", "application/sparql-query"}, - { "rs", "application/rls-services+xml"}, - { "rsd", "application/rsd+xml"}, - { "rss", "application/rss+xml"}, - { "rtf", "application/rtf"}, - { "rtx", "text/richtext"}, - { "saf", "application/vnd.yamaha.smaf-audio"}, - { "sbml", "application/sbml+xml"}, - { "sc", "application/vnd.ibm.secure-container"}, - { "scd", "application/x-msschedule"}, - { "scm", "application/vnd.lotus-screencam"}, - { "scq", "application/scvp-cv-request"}, - { "scs", "application/scvp-cv-response"}, - { "sct", "text/scriptlet"}, - { "scurl", "text/vnd.curl.scurl"}, - { "sda", "application/vnd.stardivision.draw"}, - { "sdc", "application/vnd.stardivision.calc"}, - { "sdd", "application/vnd.stardivision.impress"}, - { "sdkd", "application/vnd.solent.sdkm+xml"}, - { "sdkm", "application/vnd.solent.sdkm+xml"}, - { "sdp", "application/sdp"}, - { "sdw", "application/vnd.stardivision.writer"}, - { "see", "application/vnd.seemail"}, - { "seed", "application/vnd.fdsn.seed"}, - { "sema", "application/vnd.sema"}, - { "semd", "application/vnd.semd"}, - { "semf", "application/vnd.semf"}, - { "ser", "application/java-serialized-object"}, - { "setpay", "application/set-payment-initiation"}, - { "setreg", "application/set-registration-initiation"}, - { "sfd-hdstx", "application/vnd.hydrostatix.sof-data"}, - { "sfs", "application/vnd.spotfire.sfs"}, - { "sgl", "application/vnd.stardivision.writer-global"}, - { "sgml", "text/sgml"}, - { "sgm", "text/sgml"}, - { "sh", "application/x-sh"}, - { "shar", "application/x-shar"}, - { "shf", "application/shf+xml"}, - { "sic", "application/vnd.wap.sic"}, - { "sig", "application/pgp-signature"}, - { "silo", "model/mesh"}, - { "sis", "application/vnd.symbian.install"}, - { "sisx", "application/vnd.symbian.install"}, - { "sit", "application/x-stuffit"}, - { "si", "text/vnd.wap.si"}, - { "sitx", "application/x-stuffitx"}, - { "skd", "application/vnd.koan"}, - { "skm", "application/vnd.koan"}, - { "skp", "application/vnd.koan"}, - { "skt", "application/vnd.koan"}, - { "slc", "application/vnd.wap.slc"}, - { "sldm", "application/vnd.ms-powerpoint.slide.macroenabled.12"}, - { "sldx", "application/vnd.openxmlformats-officedocument.presentationml.slide"}, - { "slt", "application/vnd.epson.salt"}, - { "sl", "text/vnd.wap.sl"}, - { "smf", "application/vnd.stardivision.math"}, - { "smi", "application/smil+xml"}, - { "smil", "application/smil+xml"}, - { "snd", "audio/basic"}, - { "snf", "application/x-font-snf"}, - { "so", "application/octet-stream"}, - { "spc", "application/x-pkcs7-certificates"}, - { "spf", "application/vnd.yamaha.smaf-phrase"}, - { "spl", "application/x-futuresplash"}, - { "spot", "text/vnd.in3d.spot"}, - { "spp", "application/scvp-vp-response"}, - { "spq", "application/scvp-vp-request"}, - { "spx", "audio/ogg"}, - { "src", "application/x-wais-source"}, - { "srx", "application/sparql-results+xml"}, - { "sse", "application/vnd.kodak-descriptor"}, - { "ssf", "application/vnd.epson.ssf"}, - { "ssml", "application/ssml+xml"}, - { "sst", "application/vnd.ms-pkicertstore"}, - { "stc", "application/vnd.sun.xml.calc.template"}, - { "std", "application/vnd.sun.xml.draw.template"}, - { "s", "text/x-asm"}, - { "stf", "application/vnd.wt.stf"}, - { "sti", "application/vnd.sun.xml.impress.template"}, - { "stk", "application/hyperstudio"}, - { "stl", "application/vnd.ms-pki.stl"}, - { "stm", "text/html"}, - { "str", "application/vnd.pg.format"}, - { "stw", "application/vnd.sun.xml.writer.template"}, - { "sus", "application/vnd.sus-calendar"}, - { "susp", "application/vnd.sus-calendar"}, - { "sv4cpio", "application/x-sv4cpio"}, - { "sv4crc", "application/x-sv4crc"}, - { "svd", "application/vnd.svd"}, - { "svg", "image/svg+xml"}, - { "svgz", "image/svg+xml"}, - { "swa", "application/x-director"}, - { "swf", "application/x-shockwave-flash"}, - { "swi", "application/vnd.arastra.swi"}, - { "sxc", "application/vnd.sun.xml.calc"}, - { "sxd", "application/vnd.sun.xml.draw"}, - { "sxg", "application/vnd.sun.xml.writer.global"}, - { "sxi", "application/vnd.sun.xml.impress"}, - { "sxm", "application/vnd.sun.xml.math"}, - { "sxw", "application/vnd.sun.xml.writer"}, - { "tao", "application/vnd.tao.intent-module-archive"}, - { "t", "application/x-troff"}, - { "tar", "application/x-tar"}, - { "tcap", "application/vnd.3gpp2.tcap"}, - { "tcl", "application/x-tcl"}, - { "teacher", "application/vnd.smart.teacher"}, - { "tex", "application/x-tex"}, - { "texi", "application/x-texinfo"}, - { "texinfo", "application/x-texinfo"}, - { "text", "text/plain"}, - { "tfm", "application/x-tex-tfm"}, - { "tgz", "application/x-gzip"}, - { "tiff", "image/tiff"}, - { "tif", "image/tiff"}, - { "tmo", "application/vnd.tmobile-livetv"}, - { "torrent", "application/x-bittorrent"}, - { "tpl", "application/vnd.groove-tool-template"}, - { "tpt", "application/vnd.trid.tpt"}, - { "tra", "application/vnd.trueapp"}, - { "trm", "application/x-msterminal"}, - { "tr", "text/troff"}, - { "tsv", "text/tab-separated-values"}, - { "ttc", "application/x-font-ttf"}, - { "ttf", "application/x-font-ttf"}, - { "twd", "application/vnd.simtech-mindmapper"}, - { "twds", "application/vnd.simtech-mindmapper"}, - { "txd", "application/vnd.genomatix.tuxedo"}, - { "txf", "application/vnd.mobius.txf"}, - { "txt", "text/plain"}, - { "u32", "application/x-authorware-bin"}, - { "udeb", "application/x-debian-package"}, - { "ufd", "application/vnd.ufdl"}, - { "ufdl", "application/vnd.ufdl"}, - { "uls", "text/iuls"}, - { "umj", "application/vnd.umajin"}, - { "unityweb", "application/vnd.unity"}, - { "uoml", "application/vnd.uoml+xml"}, - { "uris", "text/uri-list"}, - { "uri", "text/uri-list"}, - { "urls", "text/uri-list"}, - { "ustar", "application/x-ustar"}, - { "utz", "application/vnd.uiq.theme"}, - { "uu", "text/x-uuencode"}, - { "vcd", "application/x-cdlink"}, - { "vcf", "text/x-vcard"}, - { "vcg", "application/vnd.groove-vcard"}, - { "vcs", "text/x-vcalendar"}, - { "vcx", "application/vnd.vcx"}, - { "vis", "application/vnd.visionary"}, - { "viv", "video/vnd.vivo"}, - { "vor", "application/vnd.stardivision.writer"}, - { "vox", "application/x-authorware-bin"}, - { "vrml", "x-world/x-vrml"}, - { "vsd", "application/vnd.visio"}, - { "vsf", "application/vnd.vsf"}, - { "vss", "application/vnd.visio"}, - { "vst", "application/vnd.visio"}, - { "vsw", "application/vnd.visio"}, - { "vtu", "model/vnd.vtu"}, - { "vxml", "application/voicexml+xml"}, - { "w3d", "application/x-director"}, - { "wad", "application/x-doom"}, - { "wav", "audio/x-wav"}, - { "wax", "audio/x-ms-wax"}, - { "wbmp", "image/vnd.wap.wbmp"}, - { "wbs", "application/vnd.criticaltools.wbs+xml"}, - { "wbxml", "application/vnd.wap.wbxml"}, - { "wcm", "application/vnd.ms-works"}, - { "wdb", "application/vnd.ms-works"}, - { "wiz", "application/msword"}, - { "wks", "application/vnd.ms-works"}, - { "wma", "audio/x-ms-wma"}, - { "wmd", "application/x-ms-wmd"}, - { "wmf", "application/x-msmetafile"}, - { "wmlc", "application/vnd.wap.wmlc"}, - { "wmlsc", "application/vnd.wap.wmlscriptc"}, - { "wmls", "text/vnd.wap.wmlscript"}, - { "wml", "text/vnd.wap.wml"}, - { "wm", "video/x-ms-wm"}, - { "wmv", "video/x-ms-wmv"}, - { "wmx", "video/x-ms-wmx"}, - { "wmz", "application/x-ms-wmz"}, - { "wpd", "application/vnd.wordperfect"}, - { "wpl", "application/vnd.ms-wpl"}, - { "wps", "application/vnd.ms-works"}, - { "wqd", "application/vnd.wqd"}, - { "wri", "application/x-mswrite"}, - { "wrl", "x-world/x-vrml"}, - { "wrz", "x-world/x-vrml"}, - { "wsdl", "application/wsdl+xml"}, - { "wspolicy", "application/wspolicy+xml"}, - { "wtb", "application/vnd.webturbo"}, - { "wvx", "video/x-ms-wvx"}, - { "x32", "application/x-authorware-bin"}, - { "x3d", "application/vnd.hzn-3d-crossword"}, - { "xaf", "x-world/x-vrml"}, - { "xap", "application/x-silverlight-app"}, - { "xar", "application/vnd.xara"}, - { "xbap", "application/x-ms-xbap"}, - { "xbd", "application/vnd.fujixerox.docuworks.binder"}, - { "xbm", "image/x-xbitmap"}, - { "xdm", "application/vnd.syncml.dm+xml"}, - { "xdp", "application/vnd.adobe.xdp+xml"}, - { "xdw", "application/vnd.fujixerox.docuworks"}, - { "xenc", "application/xenc+xml"}, - { "xer", "application/patch-ops-error+xml"}, - { "xfdf", "application/vnd.adobe.xfdf"}, - { "xfdl", "application/vnd.xfdl"}, - { "xht", "application/xhtml+xml"}, - { "xhtml", "application/xhtml+xml"}, - { "xhvml", "application/xv+xml"}, - { "xif", "image/vnd.xiff"}, - { "xla", "application/vnd.ms-excel"}, - { "xlam", "application/vnd.ms-excel.addin.macroenabled.12"}, - { "xlb", "application/vnd.ms-excel"}, - { "xlc", "application/vnd.ms-excel"}, - { "xlm", "application/vnd.ms-excel"}, - { "xls", "application/vnd.ms-excel"}, - { "xlsb", "application/vnd.ms-excel.sheet.binary.macroenabled.12"}, - { "xlsm", "application/vnd.ms-excel.sheet.macroenabled.12"}, - { "xlsx", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"}, - { "xlt", "application/vnd.ms-excel"}, - { "xltm", "application/vnd.ms-excel.template.macroenabled.12"}, - { "xltx", "application/vnd.openxmlformats-officedocument.spreadsheetml.template"}, - { "xlw", "application/vnd.ms-excel"}, - { "xml", "application/xml"}, - { "xo", "application/vnd.olpc-sugar"}, - { "xof", "x-world/x-vrml"}, - { "xop", "application/xop+xml"}, - { "xpdl", "application/xml"}, - { "xpi", "application/x-xpinstall"}, - { "xpm", "image/x-xpixmap"}, - { "xpr", "application/vnd.is-xpr"}, - { "xps", "application/vnd.ms-xpsdocument"}, - { "xpw", "application/vnd.intercon.formnet"}, - { "xpx", "application/vnd.intercon.formnet"}, - { "xsl", "application/xml"}, - { "xslt", "application/xslt+xml"}, - { "xsm", "application/vnd.syncml+xml"}, - { "xspf", "application/xspf+xml"}, - { "xul", "application/vnd.mozilla.xul+xml"}, - { "xvm", "application/xv+xml"}, - { "xvml", "application/xv+xml"}, - { "xwd", "image/x-xwindowdump"}, - { "xyz", "chemical/x-xyz"}, - { "z", "application/x-compress"}, - { "zaz", "application/vnd.zzazz.deck+xml"}, - { "zip", "application/zip"}, - { "zir", "application/vnd.zul"}, - { "zirz", "application/vnd.zul"}, - { "zmm", "application/vnd.handheld-entertainment+xml"} - }; - - public static String unfold(String s) { if (s == null) { return null; @@ -987,10 +119,6 @@ public class MimeUtility { return p.matcher(mimeType).matches(); } - public static boolean isDefaultMimeType(String mimeType) { - return isSameMimeType(mimeType, DEFAULT_ATTACHMENT_MIME_TYPE); - } - /** * Get decoded contents of a body. *

@@ -1045,40 +173,6 @@ public class MimeUtility { } } - public static String getMimeTypeByExtension(String filename) { - String returnedType = null; - String extension = null; - - if (filename != null && filename.lastIndexOf('.') != -1) { - extension = filename.substring(filename.lastIndexOf('.') + 1).toLowerCase(Locale.US); - returnedType = android.webkit.MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension); - } - // If the MIME type set by the user's mailer is application/octet-stream, try to figure - // out whether there's a sane file type extension. - if (returnedType != null && !isSameMimeType(returnedType, DEFAULT_ATTACHMENT_MIME_TYPE)) { - return returnedType; - } else if (extension != null) { - for (String[] contentTypeMapEntry : MIME_TYPE_BY_EXTENSION_MAP) { - if (contentTypeMapEntry[0].equals(extension)) { - return contentTypeMapEntry[1]; - } - } - } - - return DEFAULT_ATTACHMENT_MIME_TYPE; - } - - public static String getExtensionByMimeType(@NotNull String mimeType) { - String lowerCaseMimeType = mimeType.toLowerCase(Locale.US); - for (String[] contentTypeMapEntry : MIME_TYPE_BY_EXTENSION_MAP) { - if (contentTypeMapEntry[1].equals(lowerCaseMimeType)) { - return contentTypeMapEntry[0]; - } - } - - return null; - } - /** * Get a default content-transfer-encoding for use with a given content-type * when adding an unencoded attachment. It's possible that 8bit encodings @@ -1125,14 +219,4 @@ public class MimeUtility { public static boolean isSameMimeType(String mimeType, String otherMimeType) { return mimeType != null && mimeType.equalsIgnoreCase(otherMimeType); } - - public static boolean isSupportedImageType(String mimeType) { - return isSameMimeType(mimeType, "image/jpeg") || isSameMimeType(mimeType, "image/png") || - isSameMimeType(mimeType, "image/gif") || isSameMimeType(mimeType, "image/webp"); - } - - public static boolean isSupportedImageExtension(String filename) { - String mimeType = getMimeTypeByExtension(filename); - return isSupportedImageType(mimeType); - } } diff --git a/mail/common/src/test/java/com/fsck/k9/mail/MimeTypeTest.kt b/mail/common/src/test/java/com/fsck/k9/mail/MimeTypeTest.kt index 889495f1d4..c6074a9237 100644 --- a/mail/common/src/test/java/com/fsck/k9/mail/MimeTypeTest.kt +++ b/mail/common/src/test/java/com/fsck/k9/mail/MimeTypeTest.kt @@ -2,7 +2,6 @@ package com.fsck.k9.mail import com.fsck.k9.mail.MimeType.Companion.toMimeType import com.fsck.k9.mail.MimeType.Companion.toMimeTypeOrNull -import com.fsck.k9.mail.internet.getMimeTypes import com.google.common.truth.Truth.assertThat import org.junit.Assert.fail import org.junit.Test @@ -22,10 +21,8 @@ class MimeTypeTest { @Test fun checkListOfMimeTypes() { - for (mimeTypeString in getMimeTypes()) { - // If there's an invalid MIME type this will throw - mimeTypeString.toMimeType() - } + // TODO: Try to parse all IANA-registered media types + // https://www.iana.org/assignments/media-types/media-types.xhtml } @Test diff --git a/mail/common/src/test/java/com/fsck/k9/mail/internet/MimeUtilityHelper.kt b/mail/common/src/test/java/com/fsck/k9/mail/internet/MimeUtilityHelper.kt deleted file mode 100644 index 7fa7e2dc9c..0000000000 --- a/mail/common/src/test/java/com/fsck/k9/mail/internet/MimeUtilityHelper.kt +++ /dev/null @@ -1,5 +0,0 @@ -package com.fsck.k9.mail.internet - -fun getMimeTypes(): List { - return MimeUtility.MIME_TYPE_BY_EXTENSION_MAP.map { it[1] } -} -- GitLab From fdb8655f3a50f5bddad29399c306acf33180a1a0 Mon Sep 17 00:00:00 2001 From: cketti Date: Mon, 2 May 2022 15:39:41 +0200 Subject: [PATCH 44/75] Remove Robolectric from a lot of tests --- .../com/fsck/k9/helper/ListHeadersTest.java | 6 ++---- .../java/com/fsck/k9/helper/MailToTest.java | 17 +++++++--------- mail/common/build.gradle | 1 - .../java/com/fsck/k9/mail/AddressTest.java | 2 -- .../com/fsck/k9/mail/Address_quoteAtoms.java | 2 -- .../test/java/com/fsck/k9/mail/MessageTest.kt | 16 ++++++++++----- .../mail/internet/AddressHeaderBuilderTest.kt | 3 --- .../k9/mail/internet/CharsetSupportTest.java | 4 ---- .../mail/internet/MessageIdGeneratorTest.kt | 3 --- .../mail/internet/MimeMessageParseTest.java | 11 +++------- mail/protocols/imap/build.gradle | 1 - .../store/imap/RealImapConnectionTest.java | 5 ----- .../k9/mail/store/imap/RealImapFolderTest.kt | 17 +++++++++++----- mail/protocols/pop3/build.gradle | 1 - mail/protocols/smtp/build.gradle | 1 - .../mail/transport/smtp/SmtpTransportTest.kt | 3 --- .../k9/mail/store/webdav/WebDavStoreTest.java | 8 ++++++-- mail/testing/build.gradle | 1 - .../k9/mail/K9LibRobolectricTestRunner.java | 20 ------------------- 19 files changed, 41 insertions(+), 81 deletions(-) delete mode 100644 mail/testing/src/main/java/com/fsck/k9/mail/K9LibRobolectricTestRunner.java diff --git a/app/core/src/test/java/com/fsck/k9/helper/ListHeadersTest.java b/app/core/src/test/java/com/fsck/k9/helper/ListHeadersTest.java index b3d77e2ca9..1f0fdfcd6e 100644 --- a/app/core/src/test/java/com/fsck/k9/helper/ListHeadersTest.java +++ b/app/core/src/test/java/com/fsck/k9/helper/ListHeadersTest.java @@ -1,19 +1,17 @@ package com.fsck.k9.helper; +import com.fsck.k9.RobolectricTest; import com.fsck.k9.mail.Address; -import com.fsck.k9.mail.K9LibRobolectricTestRunner; import com.fsck.k9.mail.Message; import com.fsck.k9.mail.internet.MimeMessage; import org.junit.Test; -import org.junit.runner.RunWith; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; -@RunWith(K9LibRobolectricTestRunner.class) -public class ListHeadersTest { +public class ListHeadersTest extends RobolectricTest { private static final String[] TEST_EMAIL_ADDRESSES = new String[] { "prettyandsimple@example.com", "very.common@example.com", diff --git a/app/core/src/test/java/com/fsck/k9/helper/MailToTest.java b/app/core/src/test/java/com/fsck/k9/helper/MailToTest.java index bca04c1c44..d388d4136d 100644 --- a/app/core/src/test/java/com/fsck/k9/helper/MailToTest.java +++ b/app/core/src/test/java/com/fsck/k9/helper/MailToTest.java @@ -1,20 +1,18 @@ package com.fsck.k9.helper; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + import android.net.Uri; +import com.fsck.k9.RobolectricTest; import com.fsck.k9.helper.MailTo.CaseInsensitiveParamWrapper; import com.fsck.k9.mail.Address; -import com.fsck.k9.mail.K9LibRobolectricTestRunner; - import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; -import org.junit.runner.RunWith; - -import java.util.Arrays; -import java.util.Collections; -import java.util.List; import static junit.framework.Assert.assertEquals; import static junit.framework.Assert.assertFalse; @@ -23,8 +21,7 @@ import static org.hamcrest.CoreMatchers.is; import static org.junit.Assert.assertThat; -@RunWith(K9LibRobolectricTestRunner.class) -public class MailToTest { +public class MailToTest extends RobolectricTest { @Rule public ExpectedException exception = ExpectedException.none(); @@ -235,4 +232,4 @@ public class MailToTest { } -} \ No newline at end of file +} diff --git a/mail/common/build.gradle b/mail/common/build.gradle index 55ddff0df5..43823598df 100644 --- a/mail/common/build.gradle +++ b/mail/common/build.gradle @@ -15,7 +15,6 @@ dependencies { implementation "com.squareup.moshi:moshi:${versions.moshi}" testImplementation project(":mail:testing") - testImplementation "org.robolectric:robolectric:${versions.robolectric}" testImplementation "junit:junit:${versions.junit}" testImplementation "com.google.truth:truth:${versions.truth}" testImplementation "org.mockito:mockito-inline:${versions.mockito}" diff --git a/mail/common/src/test/java/com/fsck/k9/mail/AddressTest.java b/mail/common/src/test/java/com/fsck/k9/mail/AddressTest.java index 190bfc226d..5ec0585661 100644 --- a/mail/common/src/test/java/com/fsck/k9/mail/AddressTest.java +++ b/mail/common/src/test/java/com/fsck/k9/mail/AddressTest.java @@ -2,7 +2,6 @@ package com.fsck.k9.mail; import org.junit.Test; -import org.junit.runner.RunWith; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; @@ -10,7 +9,6 @@ import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; -@RunWith(K9LibRobolectricTestRunner.class) public class AddressTest { /** * test the possibility to parse "From:" fields with no email. diff --git a/mail/common/src/test/java/com/fsck/k9/mail/Address_quoteAtoms.java b/mail/common/src/test/java/com/fsck/k9/mail/Address_quoteAtoms.java index 8c8698425e..6de516565f 100644 --- a/mail/common/src/test/java/com/fsck/k9/mail/Address_quoteAtoms.java +++ b/mail/common/src/test/java/com/fsck/k9/mail/Address_quoteAtoms.java @@ -2,12 +2,10 @@ package com.fsck.k9.mail; import org.junit.Test; -import org.junit.runner.RunWith; import static org.junit.Assert.assertEquals; -@RunWith(K9LibRobolectricTestRunner.class) public class Address_quoteAtoms { @Test public void testNoQuote() { diff --git a/mail/common/src/test/java/com/fsck/k9/mail/MessageTest.kt b/mail/common/src/test/java/com/fsck/k9/mail/MessageTest.kt index cf885b32bb..65125df5ae 100644 --- a/mail/common/src/test/java/com/fsck/k9/mail/MessageTest.kt +++ b/mail/common/src/test/java/com/fsck/k9/mail/MessageTest.kt @@ -10,24 +10,30 @@ import com.fsck.k9.mail.internet.MimeMultipart import com.fsck.k9.mail.internet.TextBody import com.google.common.truth.Truth.assertThat import java.io.ByteArrayOutputStream +import java.io.File +import java.nio.file.Files import java.util.Date import java.util.TimeZone import okio.Buffer import org.apache.james.mime4j.util.MimeUtil +import org.junit.After import org.junit.Before import org.junit.Test -import org.junit.runner.RunWith -import org.robolectric.RuntimeEnvironment -@RunWith(K9LibRobolectricTestRunner::class) class MessageTest { - private val context = RuntimeEnvironment.application + private lateinit var tempDirectory: File private var mimeBoundary: Int = 0 @Before fun setUp() { TimeZone.setDefault(TimeZone.getTimeZone("Asia/Tokyo")) - BinaryTempFileBody.setTempDirectory(context.cacheDir) + tempDirectory = Files.createTempDirectory("MessageTest").toFile() + BinaryTempFileBody.setTempDirectory(tempDirectory) + } + + @After + fun tearDown() { + tempDirectory.deleteRecursively() } @Test diff --git a/mail/common/src/test/java/com/fsck/k9/mail/internet/AddressHeaderBuilderTest.kt b/mail/common/src/test/java/com/fsck/k9/mail/internet/AddressHeaderBuilderTest.kt index 3c6f6b8167..672b8eddf8 100644 --- a/mail/common/src/test/java/com/fsck/k9/mail/internet/AddressHeaderBuilderTest.kt +++ b/mail/common/src/test/java/com/fsck/k9/mail/internet/AddressHeaderBuilderTest.kt @@ -1,13 +1,10 @@ package com.fsck.k9.mail.internet import com.fsck.k9.mail.Address -import com.fsck.k9.mail.K9LibRobolectricTestRunner import com.fsck.k9.mail.crlf import org.junit.Assert.assertEquals import org.junit.Test -import org.junit.runner.RunWith -@RunWith(K9LibRobolectricTestRunner::class) class AddressHeaderBuilderTest { @Test diff --git a/mail/common/src/test/java/com/fsck/k9/mail/internet/CharsetSupportTest.java b/mail/common/src/test/java/com/fsck/k9/mail/internet/CharsetSupportTest.java index c9205be781..d1918be580 100644 --- a/mail/common/src/test/java/com/fsck/k9/mail/internet/CharsetSupportTest.java +++ b/mail/common/src/test/java/com/fsck/k9/mail/internet/CharsetSupportTest.java @@ -1,15 +1,11 @@ package com.fsck.k9.mail.internet; -import com.fsck.k9.mail.K9LibRobolectricTestRunner; - import org.junit.Test; -import org.junit.runner.RunWith; import static org.junit.Assert.assertEquals; -@RunWith(K9LibRobolectricTestRunner.class) public class CharsetSupportTest { @Test diff --git a/mail/common/src/test/java/com/fsck/k9/mail/internet/MessageIdGeneratorTest.kt b/mail/common/src/test/java/com/fsck/k9/mail/internet/MessageIdGeneratorTest.kt index 477afdfbbb..6ce7a46540 100644 --- a/mail/common/src/test/java/com/fsck/k9/mail/internet/MessageIdGeneratorTest.kt +++ b/mail/common/src/test/java/com/fsck/k9/mail/internet/MessageIdGeneratorTest.kt @@ -1,12 +1,9 @@ package com.fsck.k9.mail.internet import com.fsck.k9.mail.Address -import com.fsck.k9.mail.K9LibRobolectricTestRunner import org.junit.Assert.assertEquals import org.junit.Test -import org.junit.runner.RunWith -@RunWith(K9LibRobolectricTestRunner::class) class MessageIdGeneratorTest { private val messageIdGenerator = MessageIdGenerator( object : UuidGenerator { diff --git a/mail/common/src/test/java/com/fsck/k9/mail/internet/MimeMessageParseTest.java b/mail/common/src/test/java/com/fsck/k9/mail/internet/MimeMessageParseTest.java index 390fad798a..3046a91914 100644 --- a/mail/common/src/test/java/com/fsck/k9/mail/internet/MimeMessageParseTest.java +++ b/mail/common/src/test/java/com/fsck/k9/mail/internet/MimeMessageParseTest.java @@ -9,23 +9,18 @@ import java.util.Arrays; import java.util.Collections; import java.util.List; -import org.apache.commons.io.IOUtils; -import org.junit.Before; -import org.junit.Test; - import com.fsck.k9.mail.Address; import com.fsck.k9.mail.Body; import com.fsck.k9.mail.BodyPart; -import com.fsck.k9.mail.K9LibRobolectricTestRunner; import com.fsck.k9.mail.Message.RecipientType; import com.fsck.k9.mail.Multipart; - -import org.junit.runner.RunWith; +import org.apache.commons.io.IOUtils; +import org.junit.Before; +import org.junit.Test; import static org.junit.Assert.assertEquals; -@RunWith(K9LibRobolectricTestRunner.class) public class MimeMessageParseTest { @Before public void setup() { diff --git a/mail/protocols/imap/build.gradle b/mail/protocols/imap/build.gradle index 683d0155b0..09597e2b6f 100644 --- a/mail/protocols/imap/build.gradle +++ b/mail/protocols/imap/build.gradle @@ -13,7 +13,6 @@ dependencies { implementation "commons-io:commons-io:${versions.commonsIo}" testImplementation project(":mail:testing") - testImplementation "org.robolectric:robolectric:${versions.robolectric}" testImplementation "junit:junit:${versions.junit}" testImplementation "com.google.truth:truth:${versions.truth}" testImplementation "org.mockito:mockito-core:${versions.mockito}" diff --git a/mail/protocols/imap/src/test/java/com/fsck/k9/mail/store/imap/RealImapConnectionTest.java b/mail/protocols/imap/src/test/java/com/fsck/k9/mail/store/imap/RealImapConnectionTest.java index e707fce1ce..fc8764c621 100644 --- a/mail/protocols/imap/src/test/java/com/fsck/k9/mail/store/imap/RealImapConnectionTest.java +++ b/mail/protocols/imap/src/test/java/com/fsck/k9/mail/store/imap/RealImapConnectionTest.java @@ -10,7 +10,6 @@ import com.fsck.k9.mail.AuthenticationFailedException; import com.fsck.k9.mail.CertificateValidationException; import com.fsck.k9.mail.CertificateValidationException.Reason; import com.fsck.k9.mail.ConnectionSecurity; -import com.fsck.k9.mail.K9LibRobolectricTestRunner; import com.fsck.k9.mail.K9MailLib; import com.fsck.k9.mail.MessagingException; import com.fsck.k9.mail.XOAuth2ChallengeParserTest; @@ -21,8 +20,6 @@ import com.fsck.k9.mail.store.imap.mockserver.MockImapServer; import okio.ByteString; import org.junit.Before; import org.junit.Test; -import org.junit.runner.RunWith; -import org.robolectric.shadows.ShadowLog; import static org.hamcrest.core.StringContains.containsString; import static org.junit.Assert.assertEquals; @@ -32,7 +29,6 @@ import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; -@RunWith(K9LibRobolectricTestRunner.class) public class RealImapConnectionTest { private static final boolean DEBUGGING = false; @@ -63,7 +59,6 @@ public class RealImapConnectionTest { settings.setPassword(PASSWORD); if (DEBUGGING) { - ShadowLog.stream = System.out; K9MailLib.setDebug(true); K9MailLib.setDebugSensitive(true); } 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 9e5866a50b..558ff33e05 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 @@ -4,7 +4,6 @@ import com.fsck.k9.mail.Body import com.fsck.k9.mail.DefaultBodyFactory import com.fsck.k9.mail.FetchProfile import com.fsck.k9.mail.Flag -import com.fsck.k9.mail.K9LibRobolectricTestRunner import com.fsck.k9.mail.MessageRetrievalListener import com.fsck.k9.mail.MessagingException import com.fsck.k9.mail.Part @@ -12,11 +11,14 @@ import com.fsck.k9.mail.internet.BinaryTempFileBody import com.fsck.k9.mail.internet.MimeHeader import com.fsck.k9.mail.store.imap.ImapResponseHelper.createImapResponse import com.google.common.truth.Truth.assertThat +import java.io.File import java.io.IOException +import java.nio.file.Files import java.util.Date import java.util.TimeZone import okio.Buffer import org.apache.james.mime4j.util.MimeUtil +import org.junit.After import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse import org.junit.Assert.assertNotNull @@ -25,7 +27,6 @@ import org.junit.Assert.assertTrue import org.junit.Assert.fail import org.junit.Before import org.junit.Test -import org.junit.runner.RunWith import org.mockito.ArgumentMatchers.anySet import org.mockito.ArgumentMatchers.anyString import org.mockito.ArgumentMatchers.eq @@ -40,9 +41,7 @@ import org.mockito.kotlin.doReturn import org.mockito.kotlin.doThrow import org.mockito.kotlin.mock import org.mockito.kotlin.whenever -import org.robolectric.RuntimeEnvironment -@RunWith(K9LibRobolectricTestRunner::class) class RealImapFolderTest { private val internalImapStore = object : InternalImapStore { override val logLabel = "Account" @@ -52,9 +51,17 @@ class RealImapFolderTest { private val imapConnection = mock() private val testConnectionManager = TestConnectionManager(imapConnection) + private lateinit var tempDirectory: File + @Before fun setUp() { - BinaryTempFileBody.setTempDirectory(RuntimeEnvironment.application.cacheDir) + tempDirectory = Files.createTempDirectory("RealImapFolderTest").toFile() + BinaryTempFileBody.setTempDirectory(tempDirectory) + } + + @After + fun tearDown() { + tempDirectory.deleteRecursively() } @Test diff --git a/mail/protocols/pop3/build.gradle b/mail/protocols/pop3/build.gradle index d041d7afbd..37d6a85365 100644 --- a/mail/protocols/pop3/build.gradle +++ b/mail/protocols/pop3/build.gradle @@ -8,7 +8,6 @@ dependencies { api project(":mail:common") testImplementation project(":mail:testing") - testImplementation "org.robolectric:robolectric:${versions.robolectric}" testImplementation "junit:junit:${versions.junit}" testImplementation "com.google.truth:truth:${versions.truth}" testImplementation "org.mockito:mockito-core:${versions.mockito}" diff --git a/mail/protocols/smtp/build.gradle b/mail/protocols/smtp/build.gradle index 10a602852c..d44c134cd1 100644 --- a/mail/protocols/smtp/build.gradle +++ b/mail/protocols/smtp/build.gradle @@ -12,7 +12,6 @@ dependencies { implementation "com.squareup.okio:okio:${versions.okio}" testImplementation project(":mail:testing") - testImplementation "org.robolectric:robolectric:${versions.robolectric}" testImplementation "junit:junit:${versions.junit}" testImplementation "com.google.truth:truth:${versions.truth}" testImplementation "org.mockito.kotlin:mockito-kotlin:${versions.mockitoKotlin}" diff --git a/mail/protocols/smtp/src/test/java/com/fsck/k9/mail/transport/smtp/SmtpTransportTest.kt b/mail/protocols/smtp/src/test/java/com/fsck/k9/mail/transport/smtp/SmtpTransportTest.kt index ed9ae481d4..be50836875 100644 --- a/mail/protocols/smtp/src/test/java/com/fsck/k9/mail/transport/smtp/SmtpTransportTest.kt +++ b/mail/protocols/smtp/src/test/java/com/fsck/k9/mail/transport/smtp/SmtpTransportTest.kt @@ -4,7 +4,6 @@ import com.fsck.k9.mail.AuthType import com.fsck.k9.mail.AuthenticationFailedException import com.fsck.k9.mail.CertificateValidationException import com.fsck.k9.mail.ConnectionSecurity -import com.fsck.k9.mail.K9LibRobolectricTestRunner import com.fsck.k9.mail.Message import com.fsck.k9.mail.MessagingException import com.fsck.k9.mail.ServerSettings @@ -18,7 +17,6 @@ import com.fsck.k9.mail.transport.mockServer.MockSmtpServer import com.google.common.truth.Truth.assertThat import org.junit.Assert.fail import org.junit.Test -import org.junit.runner.RunWith import org.mockito.ArgumentMatchers.anyLong import org.mockito.ArgumentMatchers.anyString import org.mockito.kotlin.doReturn @@ -32,7 +30,6 @@ private const val USERNAME = "user" private const val PASSWORD = "password" private val CLIENT_CERTIFICATE_ALIAS: String? = null -@RunWith(K9LibRobolectricTestRunner::class) class SmtpTransportTest { private val socketFactory = TestTrustedSocketFactory.newInstance() private val oAuth2TokenProvider = createMockOAuth2TokenProvider() diff --git a/mail/protocols/webdav/src/test/java/com/fsck/k9/mail/store/webdav/WebDavStoreTest.java b/mail/protocols/webdav/src/test/java/com/fsck/k9/mail/store/webdav/WebDavStoreTest.java index 8cb66466e2..153e22c0f0 100644 --- a/mail/protocols/webdav/src/test/java/com/fsck/k9/mail/store/webdav/WebDavStoreTest.java +++ b/mail/protocols/webdav/src/test/java/com/fsck/k9/mail/store/webdav/WebDavStoreTest.java @@ -8,11 +8,12 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import android.app.Application; + import com.fsck.k9.mail.AuthType; import com.fsck.k9.mail.CertificateValidationException; import com.fsck.k9.mail.ConnectionSecurity; import com.fsck.k9.mail.FolderType; -import com.fsck.k9.mail.K9LibRobolectricTestRunner; import com.fsck.k9.mail.MessagingException; import com.fsck.k9.mail.ServerSettings; import com.fsck.k9.mail.filter.Base64; @@ -44,6 +45,8 @@ import org.mockito.MockitoAnnotations; import org.mockito.invocation.InvocationOnMock; import org.mockito.stubbing.Answer; import org.mockito.stubbing.OngoingStubbing; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; import static junit.framework.Assert.assertSame; import static org.junit.Assert.assertEquals; @@ -52,7 +55,8 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; -@RunWith(K9LibRobolectricTestRunner.class) +@RunWith(RobolectricTestRunner.class) +@Config(application = Application.class) public class WebDavStoreTest { private static final HttpResponse OK_200_RESPONSE = createOkResponse(); private static final HttpResponse UNAUTHORIZED_401_RESPONSE = createResponse(401); diff --git a/mail/testing/build.gradle b/mail/testing/build.gradle index 7f6ed262b7..0c75485445 100644 --- a/mail/testing/build.gradle +++ b/mail/testing/build.gradle @@ -5,7 +5,6 @@ dependencies { api project(":mail:common") api "com.squareup.okio:okio:${versions.okio}" - api "org.robolectric:robolectric:${versions.robolectric}" api "junit:junit:${versions.junit}" } diff --git a/mail/testing/src/main/java/com/fsck/k9/mail/K9LibRobolectricTestRunner.java b/mail/testing/src/main/java/com/fsck/k9/mail/K9LibRobolectricTestRunner.java deleted file mode 100644 index 561c594e87..0000000000 --- a/mail/testing/src/main/java/com/fsck/k9/mail/K9LibRobolectricTestRunner.java +++ /dev/null @@ -1,20 +0,0 @@ -package com.fsck.k9.mail; - -import org.junit.runners.model.InitializationError; -import org.robolectric.RobolectricTestRunner; -import org.robolectric.annotation.Config; - -public class K9LibRobolectricTestRunner extends RobolectricTestRunner { - - public K9LibRobolectricTestRunner(Class testClass) throws InitializationError { - super(testClass); - } - - @Override - protected Config buildGlobalConfig() { - return new Config.Builder() - .setSdk(22) - .setManifest(Config.NONE) - .build(); - } -} \ No newline at end of file -- GitLab From 171c0eca434520030e06f2427b9022df2f4f396a Mon Sep 17 00:00:00 2001 From: cketti Date: Mon, 2 May 2022 19:23:38 +0200 Subject: [PATCH 45/75] Use `DefaultHostnameVerifier` from Apache's httpclient5 --- mail/common/build.gradle | 3 +++ .../main/java/com/fsck/k9/mail/ssl/TrustManagerFactory.java | 6 ++++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/mail/common/build.gradle b/mail/common/build.gradle index 43823598df..5db6a9ecaa 100644 --- a/mail/common/build.gradle +++ b/mail/common/build.gradle @@ -14,6 +14,9 @@ dependencies { implementation "commons-io:commons-io:${versions.commonsIo}" implementation "com.squareup.moshi:moshi:${versions.moshi}" + // We're only using this for its DefaultHostnameVerifier + implementation "org.apache.httpcomponents.client5:httpclient5:5.1.3" + testImplementation project(":mail:testing") testImplementation "junit:junit:${versions.junit}" testImplementation "com.google.truth:truth:${versions.truth}" diff --git a/mail/common/src/main/java/com/fsck/k9/mail/ssl/TrustManagerFactory.java b/mail/common/src/main/java/com/fsck/k9/mail/ssl/TrustManagerFactory.java index 0f6212f372..36f2bd963e 100644 --- a/mail/common/src/main/java/com/fsck/k9/mail/ssl/TrustManagerFactory.java +++ b/mail/common/src/main/java/com/fsck/k9/mail/ssl/TrustManagerFactory.java @@ -15,7 +15,7 @@ import com.fsck.k9.mail.CertificateChainException; import javax.net.ssl.SSLException; import javax.net.ssl.TrustManager; import javax.net.ssl.X509TrustManager; -import org.apache.http.conn.ssl.StrictHostnameVerifier; +import org.apache.hc.client5.http.ssl.DefaultHostnameVerifier; public class TrustManagerFactory { public static TrustManagerFactory createInstance(LocalKeyStore localKeyStore) { @@ -68,6 +68,8 @@ public class TrustManagerFactory { } private class SecureX509TrustManager implements X509TrustManager { + private final DefaultHostnameVerifier hostnameVerifier = new DefaultHostnameVerifier(); + private final String mHost; private final int mPort; @@ -90,7 +92,7 @@ public class TrustManagerFactory { try { defaultTrustManager.checkServerTrusted(chain, authType); - new StrictHostnameVerifier().verify(mHost, certificate); + hostnameVerifier.verify(mHost, certificate); return; } catch (CertificateException e) { // cert. chain can't be validated -- GitLab From e7032255c62767dceb8ad367d30d5093a585359f Mon Sep 17 00:00:00 2001 From: cketti Date: Mon, 2 May 2022 19:41:43 +0200 Subject: [PATCH 46/75] JVM-ify mail libraries --- backend/webdav/build.gradle | 3 ++ mail/common/build.gradle | 40 ++++-------------- mail/protocols/imap/build.gradle | 38 ++++------------- mail/protocols/pop3/build.gradle | 32 +++----------- mail/protocols/smtp/build.gradle | 34 ++++----------- mail/protocols/webdav/build.gradle | 42 ++++--------------- .../k9/mail/store/webdav/WebDavStoreTest.java | 8 ---- mail/testing/build.gradle | 35 +++++----------- 8 files changed, 49 insertions(+), 183 deletions(-) diff --git a/backend/webdav/build.gradle b/backend/webdav/build.gradle index cca7b0a4ce..b4801f3032 100644 --- a/backend/webdav/build.gradle +++ b/backend/webdav/build.gradle @@ -24,6 +24,9 @@ android { minSdkVersion buildConfig.minSdk } + // for using Apache HTTP Client + useLibrary 'org.apache.http.legacy' + buildTypes { debug { testCoverageEnabled rootProject.testCoverage diff --git a/mail/common/build.gradle b/mail/common/build.gradle index 5db6a9ecaa..bf282669e6 100644 --- a/mail/common/build.gradle +++ b/mail/common/build.gradle @@ -1,10 +1,15 @@ -apply plugin: 'com.android.library' -apply plugin: 'org.jetbrains.kotlin.android' +apply plugin: 'java-library' +apply plugin: 'kotlin' if (rootProject.testCoverage) { apply plugin: 'jacoco' } +java { + sourceCompatibility = javaVersion + targetCompatibility = javaVersion +} + dependencies { api "org.jetbrains:annotations:${versions.jetbrainsAnnotations}" @@ -24,34 +29,3 @@ dependencies { testImplementation "org.mockito.kotlin:mockito-kotlin:${versions.mockitoKotlin}" testImplementation "com.ibm.icu:icu4j-charset:70.1" } - -android { - compileSdkVersion buildConfig.compileSdk - buildToolsVersion buildConfig.buildTools - - defaultConfig { - minSdkVersion buildConfig.minSdk - - testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" - } - - buildTypes { - debug { - testCoverageEnabled rootProject.testCoverage - } - } - - lintOptions { - abortOnError false - lintConfig file("$rootProject.projectDir/config/lint/lint.xml") - } - - compileOptions { - sourceCompatibility javaVersion - targetCompatibility javaVersion - } - - kotlinOptions { - jvmTarget = kotlinJvmVersion - } -} diff --git a/mail/protocols/imap/build.gradle b/mail/protocols/imap/build.gradle index 09597e2b6f..f86c05f47f 100644 --- a/mail/protocols/imap/build.gradle +++ b/mail/protocols/imap/build.gradle @@ -1,10 +1,15 @@ -apply plugin: 'com.android.library' -apply plugin: 'org.jetbrains.kotlin.android' +apply plugin: 'java-library' +apply plugin: 'kotlin' if (rootProject.testCoverage) { apply plugin: 'jacoco' } +java { + sourceCompatibility = javaVersion + targetCompatibility = javaVersion +} + dependencies { api project(":mail:common") @@ -20,32 +25,3 @@ dependencies { testImplementation "com.squareup.okio:okio:${versions.okio}" testImplementation "org.apache.james:apache-mime4j-core:${versions.mime4j}" } - -android { - compileSdkVersion buildConfig.compileSdk - buildToolsVersion buildConfig.buildTools - - defaultConfig { - minSdkVersion buildConfig.minSdk - } - - buildTypes { - debug { - testCoverageEnabled rootProject.testCoverage - } - } - - lintOptions { - abortOnError false - lintConfig file("$rootProject.projectDir/config/lint/lint.xml") - } - - compileOptions { - sourceCompatibility javaVersion - targetCompatibility javaVersion - } - - kotlinOptions { - jvmTarget = kotlinJvmVersion - } -} diff --git a/mail/protocols/pop3/build.gradle b/mail/protocols/pop3/build.gradle index 37d6a85365..8f750863e6 100644 --- a/mail/protocols/pop3/build.gradle +++ b/mail/protocols/pop3/build.gradle @@ -1,9 +1,14 @@ -apply plugin: 'com.android.library' +apply plugin: 'java-library' if (rootProject.testCoverage) { apply plugin: 'jacoco' } +java { + sourceCompatibility = javaVersion + targetCompatibility = javaVersion +} + dependencies { api project(":mail:common") @@ -15,28 +20,3 @@ dependencies { testImplementation "com.jcraft:jzlib:1.0.7" testImplementation "commons-io:commons-io:${versions.commonsIo}" } - -android { - compileSdkVersion buildConfig.compileSdk - buildToolsVersion buildConfig.buildTools - - defaultConfig { - minSdkVersion buildConfig.minSdk - } - - buildTypes { - debug { - testCoverageEnabled rootProject.testCoverage - } - } - - lintOptions { - abortOnError false - lintConfig file("$rootProject.projectDir/config/lint/lint.xml") - } - - compileOptions { - sourceCompatibility javaVersion - targetCompatibility javaVersion - } -} diff --git a/mail/protocols/smtp/build.gradle b/mail/protocols/smtp/build.gradle index d44c134cd1..fde2b46a9d 100644 --- a/mail/protocols/smtp/build.gradle +++ b/mail/protocols/smtp/build.gradle @@ -1,10 +1,15 @@ -apply plugin: 'com.android.library' -apply plugin: 'org.jetbrains.kotlin.android' +apply plugin: 'java-library' +apply plugin: 'kotlin' if (rootProject.testCoverage) { apply plugin: 'jacoco' } +java { + sourceCompatibility = javaVersion + targetCompatibility = javaVersion +} + dependencies { api project(":mail:common") @@ -18,28 +23,3 @@ dependencies { testImplementation "com.squareup.okio:okio:${versions.okio}" testImplementation "com.jcraft:jzlib:1.0.7" } - -android { - compileSdkVersion buildConfig.compileSdk - buildToolsVersion buildConfig.buildTools - - defaultConfig { - minSdkVersion buildConfig.minSdk - } - - buildTypes { - debug { - testCoverageEnabled rootProject.testCoverage - } - } - - lintOptions { - abortOnError false - lintConfig file("$rootProject.projectDir/config/lint/lint.xml") - } - - compileOptions { - sourceCompatibility javaVersion - targetCompatibility javaVersion - } -} diff --git a/mail/protocols/webdav/build.gradle b/mail/protocols/webdav/build.gradle index 5efe157ac8..f7af9dcd20 100644 --- a/mail/protocols/webdav/build.gradle +++ b/mail/protocols/webdav/build.gradle @@ -1,49 +1,23 @@ -apply plugin: 'com.android.library' +apply plugin: 'java-library' if (rootProject.testCoverage) { apply plugin: 'jacoco' } +java { + sourceCompatibility = javaVersion + targetCompatibility = javaVersion +} + dependencies { api project(":mail:common") implementation "commons-io:commons-io:${versions.commonsIo}" + compileOnly "org.apache.httpcomponents:httpclient:4.5.5" testImplementation project(":mail:testing") - testImplementation "org.robolectric:robolectric:${versions.robolectric}" testImplementation "junit:junit:${versions.junit}" testImplementation "com.google.truth:truth:${versions.truth}" - testImplementation "org.mockito:mockito-core:${versions.mockito}" - - // The Android Gradle plugin doesn't seem to put the Apache HTTP Client on the runtime classpath anymore when - // running JVM tests. + testImplementation "org.mockito:mockito-inline:${versions.mockito}" testImplementation "org.apache.httpcomponents:httpclient:4.5.5" } - -android { - compileSdkVersion buildConfig.compileSdk - buildToolsVersion buildConfig.buildTools - - defaultConfig { - minSdkVersion buildConfig.minSdk - } - - // for using Apache HTTP Client - useLibrary 'org.apache.http.legacy' - - buildTypes { - debug { - testCoverageEnabled rootProject.testCoverage - } - } - - lintOptions { - abortOnError false - lintConfig file("$rootProject.projectDir/config/lint/lint.xml") - } - - compileOptions { - sourceCompatibility javaVersion - targetCompatibility javaVersion - } -} diff --git a/mail/protocols/webdav/src/test/java/com/fsck/k9/mail/store/webdav/WebDavStoreTest.java b/mail/protocols/webdav/src/test/java/com/fsck/k9/mail/store/webdav/WebDavStoreTest.java index 153e22c0f0..0c1b464d2c 100644 --- a/mail/protocols/webdav/src/test/java/com/fsck/k9/mail/store/webdav/WebDavStoreTest.java +++ b/mail/protocols/webdav/src/test/java/com/fsck/k9/mail/store/webdav/WebDavStoreTest.java @@ -8,8 +8,6 @@ import java.util.HashMap; import java.util.List; import java.util.Map; -import android.app.Application; - import com.fsck.k9.mail.AuthType; import com.fsck.k9.mail.CertificateValidationException; import com.fsck.k9.mail.ConnectionSecurity; @@ -18,7 +16,6 @@ import com.fsck.k9.mail.MessagingException; import com.fsck.k9.mail.ServerSettings; import com.fsck.k9.mail.filter.Base64; import com.fsck.k9.mail.ssl.TrustManagerFactory; - import javax.net.ssl.SSLException; import org.apache.http.HttpEntity; import org.apache.http.HttpResponse; @@ -38,15 +35,12 @@ import org.apache.http.params.HttpParams; import org.apache.http.protocol.HttpContext; import org.junit.Before; import org.junit.Test; -import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.mockito.invocation.InvocationOnMock; import org.mockito.stubbing.Answer; import org.mockito.stubbing.OngoingStubbing; -import org.robolectric.RobolectricTestRunner; -import org.robolectric.annotation.Config; import static junit.framework.Assert.assertSame; import static org.junit.Assert.assertEquals; @@ -55,8 +49,6 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; -@RunWith(RobolectricTestRunner.class) -@Config(application = Application.class) public class WebDavStoreTest { private static final HttpResponse OK_200_RESPONSE = createOkResponse(); private static final HttpResponse UNAUTHORIZED_401_RESPONSE = createResponse(401); diff --git a/mail/testing/build.gradle b/mail/testing/build.gradle index 0c75485445..c1d44d29a5 100644 --- a/mail/testing/build.gradle +++ b/mail/testing/build.gradle @@ -1,5 +1,14 @@ -apply plugin: 'com.android.library' -apply plugin: 'org.jetbrains.kotlin.android' +apply plugin: 'java-library' +apply plugin: 'kotlin' + +if (rootProject.testCoverage) { + apply plugin: 'jacoco' +} + +java { + sourceCompatibility = javaVersion + targetCompatibility = javaVersion +} dependencies { api project(":mail:common") @@ -7,25 +16,3 @@ dependencies { api "com.squareup.okio:okio:${versions.okio}" api "junit:junit:${versions.junit}" } - -android { - compileSdkVersion buildConfig.compileSdk - buildToolsVersion buildConfig.buildTools - - defaultConfig { - minSdkVersion buildConfig.minSdk - } - - lintOptions { - abortOnError false - } - - compileOptions { - sourceCompatibility javaVersion - targetCompatibility javaVersion - } - - kotlinOptions { - jvmTarget = kotlinJvmVersion - } -} -- GitLab From f26d0410ba59f2af8d6c4bc8180c76e3c72d3753 Mon Sep 17 00:00:00 2001 From: cketti Date: Wed, 11 May 2022 00:41:02 +0200 Subject: [PATCH 47/75] Rename .java to .kt --- .../{RealImapConnectionTest.java => RealImapConnectionTest.kt} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename mail/protocols/imap/src/test/java/com/fsck/k9/mail/store/imap/{RealImapConnectionTest.java => RealImapConnectionTest.kt} (100%) diff --git a/mail/protocols/imap/src/test/java/com/fsck/k9/mail/store/imap/RealImapConnectionTest.java b/mail/protocols/imap/src/test/java/com/fsck/k9/mail/store/imap/RealImapConnectionTest.kt similarity index 100% rename from mail/protocols/imap/src/test/java/com/fsck/k9/mail/store/imap/RealImapConnectionTest.java rename to mail/protocols/imap/src/test/java/com/fsck/k9/mail/store/imap/RealImapConnectionTest.kt -- GitLab From bf6ae4865827086594bddb51b0c4aea27b547718 Mon Sep 17 00:00:00 2001 From: cketti Date: Wed, 11 May 2022 00:41:02 +0200 Subject: [PATCH 48/75] Convert `RealImapConnectionTest` to Kotlin --- .../mail/store/imap/RealImapConnectionTest.kt | 1528 ++++++++--------- 1 file changed, 756 insertions(+), 772 deletions(-) diff --git a/mail/protocols/imap/src/test/java/com/fsck/k9/mail/store/imap/RealImapConnectionTest.kt b/mail/protocols/imap/src/test/java/com/fsck/k9/mail/store/imap/RealImapConnectionTest.kt index fc8764c621..c77aadbc33 100644 --- a/mail/protocols/imap/src/test/java/com/fsck/k9/mail/store/imap/RealImapConnectionTest.kt +++ b/mail/protocols/imap/src/test/java/com/fsck/k9/mail/store/imap/RealImapConnectionTest.kt @@ -1,1068 +1,1052 @@ -package com.fsck.k9.mail.store.imap; - - -import java.io.IOException; -import java.net.UnknownHostException; -import java.util.List; - -import com.fsck.k9.mail.AuthType; -import com.fsck.k9.mail.AuthenticationFailedException; -import com.fsck.k9.mail.CertificateValidationException; -import com.fsck.k9.mail.CertificateValidationException.Reason; -import com.fsck.k9.mail.ConnectionSecurity; -import com.fsck.k9.mail.K9MailLib; -import com.fsck.k9.mail.MessagingException; -import com.fsck.k9.mail.XOAuth2ChallengeParserTest; -import com.fsck.k9.mail.helpers.TestTrustedSocketFactory; -import com.fsck.k9.mail.oauth.OAuth2TokenProvider; -import com.fsck.k9.mail.ssl.TrustedSocketFactory; -import com.fsck.k9.mail.store.imap.mockserver.MockImapServer; -import okio.ByteString; -import org.junit.Before; -import org.junit.Test; - -import static org.hamcrest.core.StringContains.containsString; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertThat; -import static org.junit.Assert.assertTrue; -import static org.junit.Assert.fail; - - -public class RealImapConnectionTest { - private static final boolean DEBUGGING = false; - - private static final String USERNAME = "user"; - private static final String PASSWORD = "123456"; - private static final int SOCKET_CONNECT_TIMEOUT = 10000; - private static final int SOCKET_READ_TIMEOUT = 10000; - private static final String XOAUTH_TOKEN = "token"; - private static final String XOAUTH_ANOTHER_TOKEN = "token2"; - private static final String XOAUTH_STRING = ByteString.encodeUtf8( - "user=" + USERNAME + "\001auth=Bearer " + XOAUTH_TOKEN + "\001\001").base64(); - private static final String XOAUTH_STRING_RETRY = ByteString.encodeUtf8( - "user=" + USERNAME + "\001auth=Bearer " + XOAUTH_ANOTHER_TOKEN + "\001\001").base64(); - - - private TrustedSocketFactory socketFactory; - private OAuth2TokenProvider oAuth2TokenProvider; - private SimpleImapSettings settings; - +package com.fsck.k9.mail.store.imap + +import com.fsck.k9.mail.AuthType +import com.fsck.k9.mail.AuthenticationFailedException +import com.fsck.k9.mail.CertificateValidationException +import com.fsck.k9.mail.ConnectionSecurity +import com.fsck.k9.mail.K9MailLib +import com.fsck.k9.mail.MessagingException +import com.fsck.k9.mail.XOAuth2ChallengeParserTest +import com.fsck.k9.mail.helpers.TestTrustedSocketFactory +import com.fsck.k9.mail.oauth.OAuth2TokenProvider +import com.fsck.k9.mail.ssl.TrustedSocketFactory +import com.fsck.k9.mail.store.imap.mockserver.MockImapServer +import com.google.common.truth.Truth.assertThat +import java.io.IOException +import java.net.UnknownHostException +import okio.ByteString.Companion.encodeUtf8 +import org.junit.Assert.fail +import org.junit.Before +import org.junit.Test + +private const val DEBUGGING = false + +private const val USERNAME = "user" +private const val PASSWORD = "123456" + +private const val SOCKET_CONNECT_TIMEOUT = 10000 +private const val SOCKET_READ_TIMEOUT = 10000 + +private const val XOAUTH_TOKEN = "token" +private const val XOAUTH_TOKEN_2 = "token2" +private val XOAUTH_STRING = "user=$USERNAME\u0001auth=Bearer $XOAUTH_TOKEN\u0001\u0001".base64() +private val XOAUTH_STRING_RETRY = "user=$USERNAME\u0001auth=Bearer $XOAUTH_TOKEN_2\u0001\u0001".base64() + +class RealImapConnectionTest { + private var socketFactory = TestTrustedSocketFactory.newInstance() + private var oAuth2TokenProvider = TestTokenProvider() + private var settings = SimpleImapSettings().apply { + username = USERNAME + password = PASSWORD + } @Before - public void setUp() throws Exception { - oAuth2TokenProvider = createOAuth2TokenProvider(); - socketFactory = TestTrustedSocketFactory.newInstance(); - - settings = new SimpleImapSettings(); - settings.setUsername(USERNAME); - settings.setPassword(PASSWORD); - + fun setUp() { if (DEBUGGING) { - K9MailLib.setDebug(true); - K9MailLib.setDebugSensitive(true); + K9MailLib.setDebug(true) + K9MailLib.setDebugSensitive(true) } } @Test - public void open_withNoCapabilitiesInInitialResponse_shouldIssuePreAuthCapabilitiesCommand() throws Exception { - settings.setAuthType(AuthType.PLAIN); - MockImapServer server = new MockImapServer(); - server.output("* OK example.org server"); - server.expect("1 CAPABILITY"); - server.output("* CAPABILITY IMAP4 IMAP4REV1 AUTH=PLAIN"); - server.output("1 OK CAPABILITY Completed"); - server.expect("2 AUTHENTICATE PLAIN"); - server.output("+"); - server.expect(ByteString.encodeUtf8("\000" + USERNAME + "\000" + PASSWORD).base64()); - server.output("2 OK Success"); - postAuthenticationDialogRequestingCapabilities(server); - ImapConnection imapConnection = startServerAndCreateImapConnection(server); - - imapConnection.open(); - - server.verifyConnectionStillOpen(); - server.verifyInteractionCompleted(); + fun `open() with no capabilities in initial response should issue pre-auth capabilities command`() { + val server = MockImapServer().apply { + output("* OK example.org server") + expect("1 CAPABILITY") + output("* CAPABILITY IMAP4 IMAP4REV1 AUTH=PLAIN") + output("1 OK CAPABILITY Completed") + expect("2 AUTHENTICATE PLAIN") + output("+") + expect("\u0000$USERNAME\u0000$PASSWORD".base64()) + output("2 OK Success") + postAuthenticationDialogRequestingCapabilities() + } + val imapConnection = startServerAndCreateImapConnection(server, authType = AuthType.PLAIN) + + imapConnection.open() + + server.verifyConnectionStillOpen() + server.verifyInteractionCompleted() } @Test - public void open_withCapabilitiesInInitialResponse_shouldNotIssuePreAuthCapabilitiesCommand() throws Exception { - settings.setAuthType(AuthType.PLAIN); - MockImapServer server = new MockImapServer(); - server.output("* OK [CAPABILITY IMAP4 IMAP4REV1 AUTH=PLAIN]"); - server.expect("1 AUTHENTICATE PLAIN"); - server.output("+"); - server.expect(ByteString.encodeUtf8("\000" + USERNAME + "\000" + PASSWORD).base64()); - server.output("1 OK Success"); - postAuthenticationDialogRequestingCapabilities(server, 2); - ImapConnection imapConnection = startServerAndCreateImapConnection(server); + fun `open() with capabilities in initial response should not issue pre-auth capabilities command`() { + val server = MockImapServer().apply { + output("* OK [CAPABILITY IMAP4 IMAP4REV1 AUTH=PLAIN]") + expect("1 AUTHENTICATE PLAIN") + output("+") + expect("\u0000$USERNAME\u0000$PASSWORD".base64()) + output("1 OK Success") + postAuthenticationDialogRequestingCapabilities(tag = 2) + } + val imapConnection = startServerAndCreateImapConnection(server, authType = AuthType.PLAIN) - imapConnection.open(); + imapConnection.open() - server.verifyConnectionStillOpen(); - server.verifyInteractionCompleted(); + server.verifyConnectionStillOpen() + server.verifyInteractionCompleted() } @Test - public void open_authPlain() throws Exception { - settings.setAuthType(AuthType.PLAIN); - MockImapServer server = new MockImapServer(); - preAuthenticationDialog(server, "AUTH=PLAIN"); - server.expect("2 AUTHENTICATE PLAIN"); - server.output("+"); - server.expect(ByteString.encodeUtf8("\000" + USERNAME + "\000" + PASSWORD).base64()); - server.output("2 OK Success"); - postAuthenticationDialogRequestingCapabilities(server); - ImapConnection imapConnection = startServerAndCreateImapConnection(server); + fun `open() AUTH PLAIN`() { + val server = MockImapServer().apply { + preAuthenticationDialog(capabilities = "AUTH=PLAIN") + expect("2 AUTHENTICATE PLAIN") + output("+") + expect("\u0000$USERNAME\u0000$PASSWORD".base64()) + output("2 OK Success") + postAuthenticationDialogRequestingCapabilities() + } + val imapConnection = startServerAndCreateImapConnection(server, authType = AuthType.PLAIN) - imapConnection.open(); + imapConnection.open() - server.verifyConnectionStillOpen(); - server.verifyInteractionCompleted(); + server.verifyConnectionStillOpen() + server.verifyInteractionCompleted() } @Test - public void open_afterCloseWasCalled_shouldThrow() throws Exception { - settings.setAuthType(AuthType.PLAIN); - MockImapServer server = new MockImapServer(); - preAuthenticationDialog(server); - server.expect("2 LOGIN \"" + USERNAME + "\" \"" + PASSWORD + "\""); - server.output("2 OK LOGIN completed"); - postAuthenticationDialogRequestingCapabilities(server); - ImapConnection imapConnection = startServerAndCreateImapConnection(server); - imapConnection.open(); - imapConnection.close(); + fun `open() after close() was called should throw`() { + val server = MockImapServer().apply { + preAuthenticationDialog() + expect("2 LOGIN \"$USERNAME\" \"$PASSWORD\"") + output("2 OK LOGIN completed") + postAuthenticationDialogRequestingCapabilities() + } + val imapConnection = startServerAndCreateImapConnection(server, authType = AuthType.PLAIN) + imapConnection.open() + imapConnection.close() try { - imapConnection.open(); - fail("Expected exception"); - } catch (IllegalStateException e) { - assertEquals("open() called after close(). Check wrapped exception to see where close() was called.", - e.getMessage()); + imapConnection.open() + fail("Expected exception") + } catch (e: IllegalStateException) { + assertThat(e).hasMessageThat() + .isEqualTo("open() called after close(). Check wrapped exception to see where close() was called.") } } @Test - public void open_authPlainWithLoginDisabled_shouldThrow() throws Exception { - settings.setAuthType(AuthType.PLAIN); - MockImapServer server = new MockImapServer(); - preAuthenticationDialog(server, "LOGINDISABLED"); - ImapConnection imapConnection = startServerAndCreateImapConnection(server); + fun `open() AUTH PLAIN with login disabled should throw`() { + val server = MockImapServer().apply { + preAuthenticationDialog(capabilities = "LOGINDISABLED") + } + val imapConnection = startServerAndCreateImapConnection(server, authType = AuthType.PLAIN) try { - imapConnection.open(); - fail("Expected exception"); - } catch (MessagingException e) { - assertEquals("Server doesn't support unencrypted passwords using AUTH=PLAIN and LOGIN is disabled.", - e.getMessage()); + imapConnection.open() + fail("Expected exception") + } catch (e: MessagingException) { + assertThat(e).hasMessageThat() + .isEqualTo("Server doesn't support unencrypted passwords using AUTH=PLAIN and LOGIN is disabled.") } - server.verifyConnectionClosed(); - server.verifyInteractionCompleted(); + server.verifyConnectionClosed() + server.verifyInteractionCompleted() } @Test - public void open_authPlainWithAuthenticationFailure_shouldFallbackToLogin() throws Exception { - settings.setAuthType(AuthType.PLAIN); - MockImapServer server = new MockImapServer(); - preAuthenticationDialog(server, "AUTH=PLAIN"); - server.expect("2 AUTHENTICATE PLAIN"); - server.output("+"); - server.expect(ByteString.encodeUtf8("\000" + USERNAME + "\000" + PASSWORD).base64()); - server.output("2 NO Login Failure"); - server.expect("3 LOGIN \"" + USERNAME + "\" \"" + PASSWORD + "\""); - server.output("3 OK LOGIN completed"); - postAuthenticationDialogRequestingCapabilities(server, 4); - ImapConnection imapConnection = startServerAndCreateImapConnection(server); + fun `open() AUTH PLAIN with authentication failure should fall back to LOGIN`() { + val server = MockImapServer().apply { + preAuthenticationDialog(capabilities = "AUTH=PLAIN") + expect("2 AUTHENTICATE PLAIN") + output("+") + expect("\u0000$USERNAME\u0000$PASSWORD".base64()) + output("2 NO Login Failure") + expect("3 LOGIN \"$USERNAME\" \"$PASSWORD\"") + output("3 OK LOGIN completed") + postAuthenticationDialogRequestingCapabilities(tag = 4) + } + val imapConnection = startServerAndCreateImapConnection(server, authType = AuthType.PLAIN) - imapConnection.open(); + imapConnection.open() - server.verifyConnectionStillOpen(); - server.verifyInteractionCompleted(); + server.verifyConnectionStillOpen() + server.verifyInteractionCompleted() } @Test - public void open_authPlainAndLoginFallbackWithAuthenticationFailure_shouldThrow() throws Exception { - settings.setAuthType(AuthType.PLAIN); - MockImapServer server = new MockImapServer(); - preAuthenticationDialog(server, "AUTH=PLAIN"); - server.expect("2 AUTHENTICATE PLAIN"); - server.output("+"); - server.expect(ByteString.encodeUtf8("\000" + USERNAME + "\000" + PASSWORD).base64()); - server.output("2 NO Login Failure"); - server.expect("3 LOGIN \"" + USERNAME + "\" \"" + PASSWORD + "\""); - server.output("3 NO Go away"); - ImapConnection imapConnection = startServerAndCreateImapConnection(server); + fun `open() AUTH PLAIN and LOGIN fallback with authentication failure should throw`() { + val server = MockImapServer().apply { + preAuthenticationDialog(capabilities = "AUTH=PLAIN") + expect("2 AUTHENTICATE PLAIN") + output("+") + expect("\u0000$USERNAME\u0000$PASSWORD".base64()) + output("2 NO Login Failure") + expect("3 LOGIN \"$USERNAME\" \"$PASSWORD\"") + output("3 NO Go away") + } + val imapConnection = startServerAndCreateImapConnection(server, authType = AuthType.PLAIN) try { - imapConnection.open(); - fail("Expected exception"); - } catch (AuthenticationFailedException e) { - //FIXME: improve exception message - assertThat(e.getMessage(), containsString("Go away")); + imapConnection.open() + fail("Expected exception") + } catch (e: AuthenticationFailedException) { + // FIXME: improve exception message + assertThat(e).hasMessageThat().contains("Go away") } - server.verifyConnectionClosed(); - server.verifyInteractionCompleted(); + server.verifyConnectionClosed() + server.verifyInteractionCompleted() } @Test - public void open_authPlainFailureAndDisconnect_shouldThrow() throws Exception { - settings.setAuthType(AuthType.PLAIN); - MockImapServer server = new MockImapServer(); - preAuthenticationDialog(server, "AUTH=PLAIN"); - server.expect("2 AUTHENTICATE PLAIN"); - server.output("+"); - server.expect(ByteString.encodeUtf8("\000" + USERNAME + "\000" + PASSWORD).base64()); - server.output("2 NO [UNAVAILABLE] Maximum number of connections from user+IP exceeded"); - server.closeConnection(); - ImapConnection imapConnection = startServerAndCreateImapConnection(server); + fun `open() AUTH PLAIN failure and disconnect should throw`() { + val server = MockImapServer().apply { + preAuthenticationDialog(capabilities = "AUTH=PLAIN") + expect("2 AUTHENTICATE PLAIN") + output("+") + expect("\u0000$USERNAME\u0000$PASSWORD".base64()) + output("2 NO [UNAVAILABLE] Maximum number of connections from user+IP exceeded") + closeConnection() + } + val imapConnection = startServerAndCreateImapConnection(server, authType = AuthType.PLAIN) try { - imapConnection.open(); - fail("Expected exception"); - } catch (NegativeImapResponseException e) { - assertThat(e.getMessage(), containsString("Maximum number of connections from user+IP exceeded")); + imapConnection.open() + fail("Expected exception") + } catch (e: NegativeImapResponseException) { + assertThat(e).hasMessageThat().contains("Maximum number of connections from user+IP exceeded") } - assertFalse(imapConnection.isConnected()); - server.verifyConnectionClosed(); - server.verifyInteractionCompleted(); + assertThat(imapConnection.isConnected).isFalse() + server.verifyConnectionClosed() + server.verifyInteractionCompleted() } @Test - public void open_authPlainWithByeResponseAndConnectionClose_shouldThrowAuthenticationFailedException() - throws Exception { - settings.setAuthType(AuthType.PLAIN); - MockImapServer server = new MockImapServer(); - preAuthenticationDialog(server, "AUTH=PLAIN"); - server.expect("2 AUTHENTICATE PLAIN"); - server.output("+"); - server.expect(ByteString.encodeUtf8("\000" + USERNAME + "\000" + PASSWORD).base64()); - server.output("* BYE Go away"); - server.output("2 NO Login Failure"); - server.closeConnection(); - ImapConnection imapConnection = startServerAndCreateImapConnection(server); + fun `open() AUTH PLAIN with BYE response and connection close should throw AuthenticationFailedException`() { + val server = MockImapServer().apply { + preAuthenticationDialog(capabilities = "AUTH=PLAIN") + expect("2 AUTHENTICATE PLAIN") + output("+") + expect("\u0000$USERNAME\u0000$PASSWORD".base64()) + output("* BYE Go away") + output("2 NO Login Failure") + closeConnection() + } + val imapConnection = startServerAndCreateImapConnection(server, authType = AuthType.PLAIN) try { - imapConnection.open(); - fail("Expected exception"); - } catch (AuthenticationFailedException e) { - //FIXME: improve exception message - assertThat(e.getMessage(), containsString("Login Failure")); + imapConnection.open() + fail("Expected exception") + } catch (e: AuthenticationFailedException) { + // FIXME: improve exception message + assertThat(e).hasMessageThat().contains("Login Failure") } - server.verifyConnectionClosed(); - server.verifyInteractionCompleted(); + server.verifyConnectionClosed() + server.verifyInteractionCompleted() } @Test - public void open_authPlainWithoutAuthPlainCapability_shouldUseLoginMethod() throws Exception { - settings.setAuthType(AuthType.PLAIN); - MockImapServer server = new MockImapServer(); - preAuthenticationDialog(server); - server.expect("2 LOGIN \"" + USERNAME + "\" \"" + PASSWORD + "\""); - server.output("2 OK LOGIN completed"); - postAuthenticationDialogRequestingCapabilities(server); - ImapConnection imapConnection = startServerAndCreateImapConnection(server); + fun `open() AUTH PLAIN without AUTH PLAIN capability should use LOGIN command`() { + val server = MockImapServer().apply { + preAuthenticationDialog() + expect("2 LOGIN \"$USERNAME\" \"$PASSWORD\"") + output("2 OK LOGIN completed") + postAuthenticationDialogRequestingCapabilities() + } + val imapConnection = startServerAndCreateImapConnection(server, authType = AuthType.PLAIN) - imapConnection.open(); + imapConnection.open() - server.verifyConnectionStillOpen(); - server.verifyInteractionCompleted(); + server.verifyConnectionStillOpen() + server.verifyInteractionCompleted() } @Test - public void open_authCramMd5() throws Exception { - settings.setAuthType(AuthType.CRAM_MD5); - MockImapServer server = new MockImapServer(); - preAuthenticationDialog(server, "AUTH=CRAM-MD5"); - server.expect("2 AUTHENTICATE CRAM-MD5"); - server.output("+ " + ByteString.encodeUtf8("<0000.000000000@example.org>").base64()); - server.expect("dXNlciA2ZjdiOTcyYjk5YTI4NDk4OTRhN2YyMmE3MGRhZDg0OQ=="); - server.output("2 OK Success"); - postAuthenticationDialogRequestingCapabilities(server); - ImapConnection imapConnection = startServerAndCreateImapConnection(server); + fun `open() AUTH CRAM-MD5`() { + val server = MockImapServer().apply { + preAuthenticationDialog(capabilities = "AUTH=CRAM-MD5") + expect("2 AUTHENTICATE CRAM-MD5") + output("+ ${"<0000.000000000@example.org>".base64()}") + expect("dXNlciA2ZjdiOTcyYjk5YTI4NDk4OTRhN2YyMmE3MGRhZDg0OQ==") + output("2 OK Success") + postAuthenticationDialogRequestingCapabilities() + } + val imapConnection = startServerAndCreateImapConnection(server, authType = AuthType.CRAM_MD5) - imapConnection.open(); + imapConnection.open() - server.verifyConnectionStillOpen(); - server.verifyInteractionCompleted(); + server.verifyConnectionStillOpen() + server.verifyInteractionCompleted() } @Test - public void open_authCramMd5WithAuthenticationFailure_shouldThrow() throws Exception { - settings.setAuthType(AuthType.CRAM_MD5); - MockImapServer server = new MockImapServer(); - preAuthenticationDialog(server, "AUTH=CRAM-MD5"); - server.expect("2 AUTHENTICATE CRAM-MD5"); - server.output("+ " + ByteString.encodeUtf8("<0000.000000000@example.org>").base64()); - server.expect("dXNlciA2ZjdiOTcyYjk5YTI4NDk4OTRhN2YyMmE3MGRhZDg0OQ=="); - server.output("2 NO Who are you?"); - ImapConnection imapConnection = startServerAndCreateImapConnection(server); + fun `open() AUTH CRAM-MD5 with authentication failure should throw`() { + val server = MockImapServer().apply { + preAuthenticationDialog(capabilities = "AUTH=CRAM-MD5") + expect("2 AUTHENTICATE CRAM-MD5") + output("+ ${"<0000.000000000@example.org>".base64()}") + expect("dXNlciA2ZjdiOTcyYjk5YTI4NDk4OTRhN2YyMmE3MGRhZDg0OQ==") + output("2 NO Who are you?") + } + val imapConnection = startServerAndCreateImapConnection(server, authType = AuthType.CRAM_MD5) try { - imapConnection.open(); - fail("Expected exception"); - } catch (AuthenticationFailedException e) { - //FIXME: improve exception message - assertThat(e.getMessage(), containsString("Who are you?")); + imapConnection.open() + fail("Expected exception") + } catch (e: AuthenticationFailedException) { + // FIXME: improve exception message + assertThat(e).hasMessageThat().contains("Who are you?") } - server.verifyConnectionClosed(); - server.verifyInteractionCompleted(); + server.verifyConnectionClosed() + server.verifyInteractionCompleted() } @Test - public void open_authCramMd5WithoutAuthCramMd5Capability_shouldThrow() throws Exception { - settings.setAuthType(AuthType.CRAM_MD5); - MockImapServer server = new MockImapServer(); - preAuthenticationDialog(server, "AUTH=PLAIN"); - ImapConnection imapConnection = startServerAndCreateImapConnection(server); + fun `open() AUTH CRAM-MD5 without AUTH CRAM-MD5 capability should throw`() { + val server = MockImapServer().apply { + preAuthenticationDialog(capabilities = "AUTH=PLAIN") + } + val imapConnection = startServerAndCreateImapConnection(server, authType = AuthType.CRAM_MD5) try { - imapConnection.open(); - fail("Expected exception"); - } catch (MessagingException e) { - assertEquals("Server doesn't support encrypted passwords using CRAM-MD5.", e.getMessage()); + imapConnection.open() + fail("Expected exception") + } catch (e: MessagingException) { + assertThat(e).hasMessageThat().isEqualTo("Server doesn't support encrypted passwords using CRAM-MD5.") } - server.verifyConnectionClosed(); - server.verifyInteractionCompleted(); + server.verifyConnectionClosed() + server.verifyInteractionCompleted() } @Test - public void open_authXoauthWithSaslIr() throws Exception { - settings.setAuthType(AuthType.XOAUTH2); - MockImapServer server = new MockImapServer(); - preAuthenticationDialog(server, "SASL-IR AUTH=XOAUTH AUTH=XOAUTH2"); - server.expect("2 AUTHENTICATE XOAUTH2 " + XOAUTH_STRING); - server.output("2 OK Success"); - postAuthenticationDialogRequestingCapabilities(server); - ImapConnection imapConnection = startServerAndCreateImapConnection(server); + fun `open() AUTH XOAUTH2 with SASL-IR`() { + val server = MockImapServer().apply { + preAuthenticationDialog(capabilities = "SASL-IR AUTH=XOAUTH AUTH=XOAUTH2") + expect("2 AUTHENTICATE XOAUTH2 $XOAUTH_STRING") + output("2 OK Success") + postAuthenticationDialogRequestingCapabilities() + } + val imapConnection = startServerAndCreateImapConnection(server, authType = AuthType.XOAUTH2) - imapConnection.open(); + imapConnection.open() - server.verifyConnectionStillOpen(); - server.verifyInteractionCompleted(); + server.verifyConnectionStillOpen() + server.verifyInteractionCompleted() } @Test - public void open_authXoauthWithSaslIrThrowsExeptionOn401Response() throws Exception { - settings.setAuthType(AuthType.XOAUTH2); - MockImapServer server = new MockImapServer(); - preAuthenticationDialog(server, "SASL-IR AUTH=XOAUTH AUTH=XOAUTH2"); - server.expect("2 AUTHENTICATE XOAUTH2 " + XOAUTH_STRING); - server.output("+ " + XOAuth2ChallengeParserTest.STATUS_401_RESPONSE); - server.expect(""); - server.output("2 NO SASL authentication failed"); - ImapConnection imapConnection = startServerAndCreateImapConnection(server); + fun `open() AUTH XOAUTH2 throws exception on 401 response`() { + val server = MockImapServer().apply { + preAuthenticationDialog(capabilities = "SASL-IR AUTH=XOAUTH AUTH=XOAUTH2") + expect("2 AUTHENTICATE XOAUTH2 $XOAUTH_STRING") + output("+ ${XOAuth2ChallengeParserTest.STATUS_401_RESPONSE}") + expect("") + output("2 NO SASL authentication failed") + } + val imapConnection = startServerAndCreateImapConnection(server, authType = AuthType.XOAUTH2) try { - imapConnection.open(); - fail(); - } catch (AuthenticationFailedException e) { - assertEquals("Command: AUTHENTICATE XOAUTH2; response: #2# [NO, SASL authentication failed]", - e.getMessage()); + imapConnection.open() + fail() + } catch (e: AuthenticationFailedException) { + assertThat(e).hasMessageThat() + .isEqualTo("Command: AUTHENTICATE XOAUTH2; response: #2# [NO, SASL authentication failed]") } } @Test - public void open_authXoauthWithSaslIrInvalidatesAndRetriesNewTokenOn400Response() throws Exception { - settings.setAuthType(AuthType.XOAUTH2); - MockImapServer server = new MockImapServer(); - preAuthenticationDialog(server, "SASL-IR AUTH=XOAUTH AUTH=XOAUTH2"); - server.expect("2 AUTHENTICATE XOAUTH2 " + XOAUTH_STRING); - server.output("+ " + XOAuth2ChallengeParserTest.STATUS_400_RESPONSE); - server.expect(""); - server.output("2 NO SASL authentication failed"); - server.expect("3 AUTHENTICATE XOAUTH2 " + XOAUTH_STRING_RETRY); - server.output("3 OK Success"); - postAuthenticationDialogRequestingCapabilities(server, 4); - ImapConnection imapConnection = startServerAndCreateImapConnection(server); + fun `open() AUTH XOAUTH2 invalidates and retries new token on 400 response`() { + val server = MockImapServer().apply { + preAuthenticationDialog(capabilities = "SASL-IR AUTH=XOAUTH AUTH=XOAUTH2") + expect("2 AUTHENTICATE XOAUTH2 $XOAUTH_STRING") + output("+ ${XOAuth2ChallengeParserTest.STATUS_400_RESPONSE}") + expect("") + output("2 NO SASL authentication failed") + expect("3 AUTHENTICATE XOAUTH2 $XOAUTH_STRING_RETRY") + output("3 OK Success") + postAuthenticationDialogRequestingCapabilities(tag = 4) + } + val imapConnection = startServerAndCreateImapConnection(server, authType = AuthType.XOAUTH2) - imapConnection.open(); + imapConnection.open() - server.verifyConnectionStillOpen(); - server.verifyInteractionCompleted(); + server.verifyConnectionStillOpen() + server.verifyInteractionCompleted() } @Test - public void open_authXoauthWithSaslIrInvalidatesAndRetriesNewTokenOnInvalidJsonResponse() throws Exception { - settings.setAuthType(AuthType.XOAUTH2); - MockImapServer server = new MockImapServer(); - preAuthenticationDialog(server, "SASL-IR AUTH=XOAUTH AUTH=XOAUTH2"); - server.expect("2 AUTHENTICATE XOAUTH2 " + XOAUTH_STRING); - server.output("+ " + XOAuth2ChallengeParserTest.INVALID_RESPONSE); - server.expect(""); - server.output("2 NO SASL authentication failed"); - server.expect("3 AUTHENTICATE XOAUTH2 " + XOAUTH_STRING_RETRY); - server.output("3 OK Success"); - requestCapabilities(server, 4); - simplePostAuthenticationDialog(server, 5); - ImapConnection imapConnection = startServerAndCreateImapConnection(server); - - imapConnection.open(); - - server.verifyConnectionStillOpen(); - server.verifyInteractionCompleted(); + fun `open() AUTH XOAUTH2 invalidates and retries new token on invalid JSON response`() { + val server = MockImapServer().apply { + preAuthenticationDialog(capabilities = "SASL-IR AUTH=XOAUTH AUTH=XOAUTH2") + expect("2 AUTHENTICATE XOAUTH2 $XOAUTH_STRING") + output("+ ${XOAuth2ChallengeParserTest.INVALID_RESPONSE}") + expect("") + output("2 NO SASL authentication failed") + expect("3 AUTHENTICATE XOAUTH2 $XOAUTH_STRING_RETRY") + output("3 OK Success") + requestCapabilities(tag = 4) + simplePostAuthenticationDialog(tag = 5) + } + val imapConnection = startServerAndCreateImapConnection(server, authType = AuthType.XOAUTH2) + + imapConnection.open() + + server.verifyConnectionStillOpen() + server.verifyInteractionCompleted() } @Test - public void open_authXoauthWithSaslIrInvalidatesAndRetriesNewTokenOnMissingStatusJsonResponse() throws Exception { - settings.setAuthType(AuthType.XOAUTH2); - MockImapServer server = new MockImapServer(); - preAuthenticationDialog(server, "SASL-IR AUTH=XOAUTH AUTH=XOAUTH2"); - server.expect("2 AUTHENTICATE XOAUTH2 " + XOAUTH_STRING); - server.output("+ " + XOAuth2ChallengeParserTest.MISSING_STATUS_RESPONSE); - server.expect(""); - server.output("2 NO SASL authentication failed"); - server.expect("3 AUTHENTICATE XOAUTH2 " + XOAUTH_STRING_RETRY); - server.output("3 OK Success"); - requestCapabilities(server, 4); - simplePostAuthenticationDialog(server, 5); - ImapConnection imapConnection = startServerAndCreateImapConnection(server); - - imapConnection.open(); - - server.verifyConnectionStillOpen(); - server.verifyInteractionCompleted(); + fun `open() AUTH XOAUTH2 invalidates and retries new token on missing status JSON response`() { + val server = MockImapServer().apply { + preAuthenticationDialog(capabilities = "SASL-IR AUTH=XOAUTH AUTH=XOAUTH2") + expect("2 AUTHENTICATE XOAUTH2 $XOAUTH_STRING") + output("+ ${XOAuth2ChallengeParserTest.MISSING_STATUS_RESPONSE}") + expect("") + output("2 NO SASL authentication failed") + expect("3 AUTHENTICATE XOAUTH2 $XOAUTH_STRING_RETRY") + output("3 OK Success") + requestCapabilities(tag = 4) + simplePostAuthenticationDialog(tag = 5) + } + val imapConnection = startServerAndCreateImapConnection(server, authType = AuthType.XOAUTH2) + + imapConnection.open() + + server.verifyConnectionStillOpen() + server.verifyInteractionCompleted() } @Test - public void open_authXoauthWithSaslIrWithOldTokenThrowsExceptionIfRetryFails() throws Exception { - settings.setAuthType(AuthType.XOAUTH2); - MockImapServer server = new MockImapServer(); - preAuthenticationDialog(server, "SASL-IR AUTH=XOAUTH AUTH=XOAUTH2"); - server.expect("2 AUTHENTICATE XOAUTH2 " + XOAUTH_STRING); - server.output("+ r3j3krj3irj3oir3ojo"); - server.expect(""); - server.output("2 NO SASL authentication failed"); - server.expect("3 AUTHENTICATE XOAUTH2 " + XOAUTH_STRING_RETRY); - server.output("+ 433ba3a3a"); - server.expect(""); - server.output("3 NO SASL authentication failed"); - postAuthenticationDialogRequestingCapabilities(server); - ImapConnection imapConnection = startServerAndCreateImapConnection(server); + fun `open() AUTH XOAUTH2 with old token throws exception if retry fails`() { + val server = MockImapServer().apply { + preAuthenticationDialog(capabilities = "SASL-IR AUTH=XOAUTH AUTH=XOAUTH2") + expect("2 AUTHENTICATE XOAUTH2 $XOAUTH_STRING") + output("+ r3j3krj3irj3oir3ojo") + expect("") + output("2 NO SASL authentication failed") + expect("3 AUTHENTICATE XOAUTH2 $XOAUTH_STRING_RETRY") + output("+ 433ba3a3a") + expect("") + output("3 NO SASL authentication failed") + postAuthenticationDialogRequestingCapabilities() + } + val imapConnection = startServerAndCreateImapConnection(server, authType = AuthType.XOAUTH2) try { - imapConnection.open(); - fail(); - } catch (AuthenticationFailedException e) { - assertEquals("Command: AUTHENTICATE XOAUTH2; response: #3# [NO, SASL authentication failed]", - e.getMessage()); + imapConnection.open() + fail() + } catch (e: AuthenticationFailedException) { + assertThat(e).hasMessageThat() + .isEqualTo("Command: AUTHENTICATE XOAUTH2; response: #3# [NO, SASL authentication failed]") } } @Test - public void open_authXoauthWithSaslIrParsesCapabilities() throws Exception { - settings.setAuthType(AuthType.XOAUTH2); - MockImapServer server = new MockImapServer(); - preAuthenticationDialog(server, "SASL-IR AUTH=XOAUTH AUTH=XOAUTH2"); - server.expect("2 AUTHENTICATE XOAUTH2 " + XOAUTH_STRING); - server.output("2 OK [CAPABILITY IMAP4REV1 IDLE XM-GM-EXT-1]"); - simplePostAuthenticationDialog(server, 3); - ImapConnection imapConnection = startServerAndCreateImapConnection(server); + fun `open() AUTH XOAUTH2 parses capabilities`() { + val server = MockImapServer().apply { + preAuthenticationDialog(capabilities = "SASL-IR AUTH=XOAUTH AUTH=XOAUTH2") + expect("2 AUTHENTICATE XOAUTH2 $XOAUTH_STRING") + output("2 OK [CAPABILITY IMAP4REV1 IDLE XM-GM-EXT-1]") + simplePostAuthenticationDialog(tag = 3) + } + val imapConnection = startServerAndCreateImapConnection(server, authType = AuthType.XOAUTH2) - imapConnection.open(); + imapConnection.open() - server.verifyConnectionStillOpen(); - server.verifyInteractionCompleted(); - assertTrue(imapConnection.hasCapability("XM-GM-EXT-1")); + server.verifyConnectionStillOpen() + server.verifyInteractionCompleted() + assertThat(imapConnection.hasCapability("XM-GM-EXT-1")).isTrue() } @Test - public void open_authExternal() throws Exception { - settings.setAuthType(AuthType.EXTERNAL); - MockImapServer server = new MockImapServer(); - preAuthenticationDialog(server, "AUTH=EXTERNAL"); - server.expect("2 AUTHENTICATE EXTERNAL " + ByteString.encodeUtf8(USERNAME).base64()); - server.output("2 OK Success"); - postAuthenticationDialogRequestingCapabilities(server); - ImapConnection imapConnection = startServerAndCreateImapConnection(server); + fun `open() AUTH EXTERNAL`() { + val server = MockImapServer().apply { + preAuthenticationDialog(capabilities = "AUTH=EXTERNAL") + expect("2 AUTHENTICATE EXTERNAL ${USERNAME.base64()}") + output("2 OK Success") + postAuthenticationDialogRequestingCapabilities() + } + val imapConnection = startServerAndCreateImapConnection(server, authType = AuthType.EXTERNAL) - imapConnection.open(); + imapConnection.open() - server.verifyConnectionStillOpen(); - server.verifyInteractionCompleted(); + server.verifyConnectionStillOpen() + server.verifyInteractionCompleted() } @Test - public void open_authExternalWithAuthenticationFailure_shouldThrow() throws Exception { - settings.setAuthType(AuthType.EXTERNAL); - MockImapServer server = new MockImapServer(); - preAuthenticationDialog(server, "AUTH=EXTERNAL"); - server.expect("2 AUTHENTICATE EXTERNAL " + ByteString.encodeUtf8(USERNAME).base64()); - server.output("2 NO Bad certificate"); - ImapConnection imapConnection = startServerAndCreateImapConnection(server); + fun `open() AUTH EXTERNAL with authentication failure should throw`() { + val server = MockImapServer().apply { + preAuthenticationDialog(capabilities = "AUTH=EXTERNAL") + expect("2 AUTHENTICATE EXTERNAL ${USERNAME.base64()}") + output("2 NO Bad certificate") + } + val imapConnection = startServerAndCreateImapConnection(server, authType = AuthType.EXTERNAL) try { - imapConnection.open(); - fail("Expected exception"); - } catch (CertificateValidationException e) { - //FIXME: improve exception message - assertThat(e.getMessage(), containsString("Bad certificate")); + imapConnection.open() + fail("Expected exception") + } catch (e: CertificateValidationException) { + // FIXME: improve exception message + assertThat(e).hasMessageThat().contains("Bad certificate") } - server.verifyConnectionClosed(); - server.verifyInteractionCompleted(); + server.verifyConnectionClosed() + server.verifyInteractionCompleted() } @Test - public void open_authExternalWithoutAuthExternalCapability_shouldThrow() throws Exception { - settings.setAuthType(AuthType.EXTERNAL); - MockImapServer server = new MockImapServer(); - preAuthenticationDialog(server, "AUTH=PLAIN"); - ImapConnection imapConnection = startServerAndCreateImapConnection(server); + fun `open() AUTH EXTERNAL without AUTH EXTERNAL capability should throw`() { + val server = MockImapServer().apply { + preAuthenticationDialog(capabilities = "AUTH=PLAIN") + } + val imapConnection = startServerAndCreateImapConnection(server, authType = AuthType.EXTERNAL) try { - imapConnection.open(); - fail("Expected exception"); - } catch (CertificateValidationException e) { - assertEquals(Reason.MissingCapability, e.getReason()); + imapConnection.open() + fail("Expected exception") + } catch (e: CertificateValidationException) { + assertThat(e.reason).isEqualTo(CertificateValidationException.Reason.MissingCapability) } - server.verifyConnectionClosed(); - server.verifyInteractionCompleted(); + server.verifyConnectionClosed() + server.verifyInteractionCompleted() } @Test - public void open_withNoPostAuthCapabilityResponse_shouldIssueCapabilityCommand() throws Exception { - settings.setAuthType(AuthType.PLAIN); - MockImapServer server = new MockImapServer(); - preAuthenticationDialog(server, "AUTH=PLAIN"); - server.expect("2 AUTHENTICATE PLAIN"); - server.output("+"); - server.expect(ByteString.encodeUtf8("\000" + USERNAME + "\000" + PASSWORD).base64()); - server.output("2 OK Success"); - server.expect("3 CAPABILITY"); - server.output("* CAPABILITY IDLE"); - server.output("3 OK CAPABILITY Completed"); - simplePostAuthenticationDialog(server, 4); - ImapConnection imapConnection = startServerAndCreateImapConnection(server); - - imapConnection.open(); - - server.verifyConnectionStillOpen(); - server.verifyInteractionCompleted(); - assertTrue(imapConnection.isIdleCapable()); + fun `open() with no post-auth CAPABILITY response should issue CAPABILITY command`() { + val server = MockImapServer().apply { + preAuthenticationDialog(capabilities = "AUTH=PLAIN") + expect("2 AUTHENTICATE PLAIN") + output("+") + expect("\u0000$USERNAME\u0000$PASSWORD".base64()) + output("2 OK Success") + expect("3 CAPABILITY") + output("* CAPABILITY IDLE") + output("3 OK CAPABILITY Completed") + simplePostAuthenticationDialog(tag = 4) + } + val imapConnection = startServerAndCreateImapConnection(server, authType = AuthType.PLAIN) + + imapConnection.open() + + server.verifyConnectionStillOpen() + server.verifyInteractionCompleted() + assertThat(imapConnection.isIdleCapable).isTrue() } @Test - public void open_withUntaggedPostAuthCapabilityResponse_shouldNotIssueCapabilityCommand() throws Exception { - settings.setAuthType(AuthType.PLAIN); - MockImapServer server = new MockImapServer(); - preAuthenticationDialog(server, "AUTH=PLAIN"); - server.expect("2 AUTHENTICATE PLAIN"); - server.output("+"); - server.expect(ByteString.encodeUtf8("\000" + USERNAME + "\000" + PASSWORD).base64()); - server.output("* CAPABILITY IMAP4rev1 UNSELECT IDLE QUOTA ID XLIST CHILDREN X-GM-EXT-1 UIDPLUS " + - "ENABLE MOVE CONDSTORE ESEARCH UTF8=ACCEPT LIST-EXTENDED LIST-STATUS LITERAL- SPECIAL-USE " + - "APPENDLIMIT=35651584"); - server.output("2 OK"); - simplePostAuthenticationDialog(server, 3); - ImapConnection imapConnection = startServerAndCreateImapConnection(server); - - imapConnection.open(); - - server.verifyConnectionStillOpen(); - server.verifyInteractionCompleted(); - assertTrue(imapConnection.isIdleCapable()); + fun `open() with untagged post-auth CAPABILITY response`() { + val server = MockImapServer().apply { + preAuthenticationDialog(capabilities = "AUTH=PLAIN") + expect("2 AUTHENTICATE PLAIN") + output("+") + expect("\u0000$USERNAME\u0000$PASSWORD".base64()) + output( + "* CAPABILITY IMAP4rev1 UNSELECT IDLE QUOTA ID XLIST CHILDREN X-GM-EXT-1 UIDPLUS " + + "ENABLE MOVE CONDSTORE ESEARCH UTF8=ACCEPT LIST-EXTENDED LIST-STATUS LITERAL- SPECIAL-USE " + + "APPENDLIMIT=35651584" + ) + output("2 OK") + simplePostAuthenticationDialog(tag = 3) + } + val imapConnection = startServerAndCreateImapConnection(server, authType = AuthType.PLAIN) + + imapConnection.open() + + server.verifyConnectionStillOpen() + server.verifyInteractionCompleted() + assertThat(imapConnection.isIdleCapable).isTrue() } @Test - public void open_withPostAuthCapabilityResponse_shouldNotIssueCapabilityCommand() throws Exception { - settings.setAuthType(AuthType.PLAIN); - MockImapServer server = new MockImapServer(); - preAuthenticationDialog(server, "AUTH=PLAIN"); - server.expect("2 AUTHENTICATE PLAIN"); - server.output("+"); - server.expect(ByteString.encodeUtf8("\000" + USERNAME + "\000" + PASSWORD).base64()); - server.output("2 OK [CAPABILITY IDLE]"); - simplePostAuthenticationDialog(server, 3); - ImapConnection imapConnection = startServerAndCreateImapConnection(server); + fun `open() with post-auth CAPABILITY response`() { + val server = MockImapServer().apply { + preAuthenticationDialog(capabilities = "AUTH=PLAIN") + expect("2 AUTHENTICATE PLAIN") + output("+") + expect("\u0000$USERNAME\u0000$PASSWORD".base64()) + output("2 OK [CAPABILITY IDLE]") + simplePostAuthenticationDialog(tag = 3) + } + val imapConnection = startServerAndCreateImapConnection(server, authType = AuthType.PLAIN) - imapConnection.open(); + imapConnection.open() - server.verifyConnectionStillOpen(); - server.verifyInteractionCompleted(); - assertTrue(imapConnection.isIdleCapable()); + server.verifyConnectionStillOpen() + server.verifyInteractionCompleted() + assertThat(imapConnection.isIdleCapable).isTrue() } @Test - public void open_withNamespaceCapability_shouldIssueNamespaceCommand() throws Exception { - MockImapServer server = new MockImapServer(); - simplePreAuthAndLoginDialog(server, "NAMESPACE"); - server.expect("3 NAMESPACE"); - server.output("* NAMESPACE ((\"\" \"/\")) NIL NIL"); - server.output("3 OK command completed"); - ImapConnection imapConnection = startServerAndCreateImapConnection(server); + fun `open() with NAMESPACE capability should issue NAMESPACE command`() { + val server = MockImapServer().apply { + simplePreAuthAndLoginDialog(postAuthCapabilities = "NAMESPACE") + expect("3 NAMESPACE") + output("* NAMESPACE ((\"\" \"/\")) NIL NIL") + output("3 OK command completed") + } + val imapConnection = startServerAndCreateImapConnection(server) - imapConnection.open(); + imapConnection.open() - server.verifyConnectionStillOpen(); - server.verifyInteractionCompleted(); + server.verifyConnectionStillOpen() + server.verifyInteractionCompleted() } @Test - public void open_withConnectionError_shouldThrow() throws Exception { - settings.setHost("127.1.2.3"); - settings.setPort(143); - ImapConnection imapConnection = createImapConnection(settings, socketFactory, oAuth2TokenProvider); + fun `open() with connection error should throw`() { + settings.host = "127.1.2.3" + settings.port = 143 + val imapConnection = createImapConnection(settings, socketFactory, oAuth2TokenProvider) try { - imapConnection.open(); - fail("Expected exception"); - } catch (MessagingException e) { - assertEquals("Cannot connect to host", e.getMessage()); - assertTrue(e.getCause() instanceof IOException); + imapConnection.open() + fail("Expected exception") + } catch (e: MessagingException) { + assertThat(e).hasMessageThat().isEqualTo("Cannot connect to host") + assertThat(e).hasCauseThat().isInstanceOf(IOException::class.java) } } @Test - public void open_withInvalidHostname_shouldThrow() throws Exception { - settings.setHost("host name"); - settings.setPort(143); - ImapConnection imapConnection = createImapConnection(settings, socketFactory, oAuth2TokenProvider); + fun `open() with invalid hostname should throw`() { + settings.host = "host name" + settings.port = 143 + val imapConnection = createImapConnection(settings, socketFactory, oAuth2TokenProvider) try { - imapConnection.open(); - fail("Expected exception"); - } catch (UnknownHostException ignored) { + imapConnection.open() + fail("Expected exception") + } catch (ignored: UnknownHostException) { } - assertFalse(imapConnection.isConnected()); + assertThat(imapConnection.isConnected).isFalse() } @Test - public void open_withStartTlsCapability_shouldIssueStartTlsCommand() throws Exception { - settings.setAuthType(AuthType.PLAIN); - settings.setConnectionSecurity(ConnectionSecurity.STARTTLS_REQUIRED); - MockImapServer server = new MockImapServer(); - preAuthenticationDialog(server, "STARTTLS LOGINDISABLED"); - server.expect("2 STARTTLS"); - server.output("2 OK [CAPABILITY IMAP4REV1 NAMESPACE]"); - server.startTls(); - server.expect("3 CAPABILITY"); - server.output("* CAPABILITY IMAP4 IMAP4REV1"); - server.output("3 OK"); - server.expect("4 LOGIN \"" + USERNAME + "\" \"" + PASSWORD + "\""); - server.output("4 OK [CAPABILITY NAMESPACE] LOGIN completed"); - server.expect("5 NAMESPACE"); - server.output("* NAMESPACE ((\"\" \"/\")) NIL NIL"); - server.output("5 OK command completed"); - ImapConnection imapConnection = startServerAndCreateImapConnection(server); - - imapConnection.open(); - - server.verifyConnectionStillOpen(); - server.verifyInteractionCompleted(); + fun `open() with STARTTLS capability should issue STARTTLS command`() { + settings.connectionSecurity = ConnectionSecurity.STARTTLS_REQUIRED + val server = MockImapServer().apply { + preAuthenticationDialog(capabilities = "STARTTLS LOGINDISABLED") + expect("2 STARTTLS") + output("2 OK [CAPABILITY IMAP4REV1 NAMESPACE]") + startTls() + expect("3 CAPABILITY") + output("* CAPABILITY IMAP4 IMAP4REV1") + output("3 OK") + expect("4 LOGIN \"$USERNAME\" \"$PASSWORD\"") + output("4 OK [CAPABILITY NAMESPACE] LOGIN completed") + expect("5 NAMESPACE") + output("* NAMESPACE ((\"\" \"/\")) NIL NIL") + output("5 OK command completed") + } + val imapConnection = startServerAndCreateImapConnection(server, authType = AuthType.PLAIN) + + imapConnection.open() + + server.verifyConnectionStillOpen() + server.verifyInteractionCompleted() } @Test - public void open_withStartTlsButWithoutStartTlsCapability_shouldThrow() throws Exception { - settings.setConnectionSecurity(ConnectionSecurity.STARTTLS_REQUIRED); - MockImapServer server = new MockImapServer(); - preAuthenticationDialog(server); - ImapConnection imapConnection = startServerAndCreateImapConnection(server); + fun `open() with STARTTLS but without STARTTLS capability should throw`() { + settings.connectionSecurity = ConnectionSecurity.STARTTLS_REQUIRED + val server = MockImapServer().apply { + preAuthenticationDialog() + } + val imapConnection = startServerAndCreateImapConnection(server) try { - imapConnection.open(); - fail("Expected exception"); - } catch (CertificateValidationException e) { - //FIXME: CertificateValidationException seems wrong - assertEquals("STARTTLS connection security not available", e.getMessage()); + imapConnection.open() + fail("Expected exception") + } catch (e: CertificateValidationException) { + // FIXME: CertificateValidationException seems wrong + assertThat(e).hasMessageThat().isEqualTo("STARTTLS connection security not available") } - server.verifyConnectionClosed(); - server.verifyInteractionCompleted(); + server.verifyConnectionClosed() + server.verifyInteractionCompleted() } @Test - public void open_withUntaggedCapabilityAfterStartTls_shouldNotThrow() throws Exception { - settings.setAuthType(AuthType.PLAIN); - settings.setConnectionSecurity(ConnectionSecurity.STARTTLS_REQUIRED); - MockImapServer server = new MockImapServer(); - preAuthenticationDialog(server, "STARTTLS LOGINDISABLED"); - server.expect("2 STARTTLS"); - server.output("2 OK Begin TLS negotiation now"); - server.startTls(); - server.output("* CAPABILITY IMAP4REV1 IMAP4"); - server.expect("3 CAPABILITY"); - server.output("* CAPABILITY IMAP4 IMAP4REV1"); - server.output("3 OK"); - server.expect("4 LOGIN \"" + USERNAME + "\" \"" + PASSWORD + "\""); - server.output("4 OK [CAPABILITY IMAP4REV1] LOGIN completed"); - simplePostAuthenticationDialog(server, 5); - ImapConnection imapConnection = startServerAndCreateImapConnection(server); - - imapConnection.open(); - - server.verifyConnectionStillOpen(); - server.verifyInteractionCompleted(); + fun `open() with untagged CAPABILITY after STARTTLS should not throw`() { + settings.connectionSecurity = ConnectionSecurity.STARTTLS_REQUIRED + val server = MockImapServer().apply { + preAuthenticationDialog(capabilities = "STARTTLS LOGINDISABLED") + expect("2 STARTTLS") + output("2 OK Begin TLS negotiation now") + startTls() + output("* CAPABILITY IMAP4REV1 IMAP4") + expect("3 CAPABILITY") + output("* CAPABILITY IMAP4 IMAP4REV1") + output("3 OK") + expect("4 LOGIN \"$USERNAME\" \"$PASSWORD\"") + output("4 OK [CAPABILITY IMAP4REV1] LOGIN completed") + simplePostAuthenticationDialog(tag = 5) + } + val imapConnection = startServerAndCreateImapConnection(server, authType = AuthType.PLAIN) + + imapConnection.open() + + server.verifyConnectionStillOpen() + server.verifyInteractionCompleted() } @Test - public void open_withNegativeResponseToStartTlsCommand_shouldThrow() throws Exception { - settings.setAuthType(AuthType.PLAIN); - settings.setConnectionSecurity(ConnectionSecurity.STARTTLS_REQUIRED); - MockImapServer server = new MockImapServer(); - preAuthenticationDialog(server, "STARTTLS"); - server.expect("2 STARTTLS"); - server.output("2 NO"); - ImapConnection imapConnection = startServerAndCreateImapConnection(server); + fun `open() with negative response to STARTTLS command should throw`() { + settings.connectionSecurity = ConnectionSecurity.STARTTLS_REQUIRED + val server = MockImapServer().apply { + preAuthenticationDialog(capabilities = "STARTTLS") + expect("2 STARTTLS") + output("2 NO") + } + val imapConnection = startServerAndCreateImapConnection(server, authType = AuthType.PLAIN) try { - imapConnection.open(); - fail("Expected exception"); - } catch (NegativeImapResponseException e) { - assertEquals(e.getMessage(), "Command: STARTTLS; response: #2# [NO]"); + imapConnection.open() + fail("Expected exception") + } catch (e: NegativeImapResponseException) { + assertThat(e).hasMessageThat().isEqualTo("Command: STARTTLS; response: #2# [NO]") } - server.verifyConnectionClosed(); - server.verifyInteractionCompleted(); + server.verifyConnectionClosed() + server.verifyInteractionCompleted() } @Test - public void open_withCompressDeflateCapability_shouldEnableCompression() throws Exception { - settings.setUseCompression(true); - MockImapServer server = new MockImapServer(); - simplePreAuthAndLoginDialog(server, "COMPRESS=DEFLATE"); - server.expect("3 COMPRESS DEFLATE"); - server.output("3 OK"); - server.enableCompression(); - simplePostAuthenticationDialog(server, 4); - ImapConnection imapConnection = startServerAndCreateImapConnection(server); + fun `open() with COMPRESS=DEFLATE capability should enable compression`() { + settings.setUseCompression(true) + val server = MockImapServer().apply { + simplePreAuthAndLoginDialog(postAuthCapabilities = "COMPRESS=DEFLATE") + expect("3 COMPRESS DEFLATE") + output("3 OK") + enableCompression() + simplePostAuthenticationDialog(tag = 4) + } + val imapConnection = startServerAndCreateImapConnection(server) - imapConnection.open(); + imapConnection.open() - server.verifyConnectionStillOpen(); - server.verifyInteractionCompleted(); + server.verifyConnectionStillOpen() + server.verifyInteractionCompleted() } @Test - public void open_withNegativeResponseToCompressionCommand_shouldContinue() throws Exception { - settings.setAuthType(AuthType.PLAIN); - settings.setUseCompression(true); - MockImapServer server = new MockImapServer(); - simplePreAuthAndLoginDialog(server, "COMPRESS=DEFLATE"); - server.expect("3 COMPRESS DEFLATE"); - server.output("3 NO"); - simplePostAuthenticationDialog(server, 4); - ImapConnection imapConnection = startServerAndCreateImapConnection(server); + fun `open() with negative response to COMPRESS command should continue`() { + settings.setUseCompression(true) + val server = MockImapServer().apply { + simplePreAuthAndLoginDialog(postAuthCapabilities = "COMPRESS=DEFLATE") + expect("3 COMPRESS DEFLATE") + output("3 NO") + simplePostAuthenticationDialog(tag = 4) + } + val imapConnection = startServerAndCreateImapConnection(server) - imapConnection.open(); + imapConnection.open() - server.verifyConnectionStillOpen(); - server.verifyInteractionCompleted(); + server.verifyConnectionStillOpen() + server.verifyInteractionCompleted() } @Test - public void open_withIoExceptionDuringCompressionCommand_shouldThrow() throws Exception { - settings.setAuthType(AuthType.PLAIN); - settings.setUseCompression(true); - MockImapServer server = new MockImapServer(); - simplePreAuthAndLoginDialog(server, "COMPRESS=DEFLATE"); - server.expect("3 COMPRESS DEFLATE"); - server.closeConnection(); - ImapConnection imapConnection = startServerAndCreateImapConnection(server); + fun `open() with IOException during COMPRESS command should throw`() { + settings.setUseCompression(true) + val server = MockImapServer().apply { + simplePreAuthAndLoginDialog(postAuthCapabilities = "COMPRESS=DEFLATE") + expect("3 COMPRESS DEFLATE") + closeConnection() + } + val imapConnection = startServerAndCreateImapConnection(server) try { - imapConnection.open(); - fail("Exception expected"); - } catch (IOException ignored) { + imapConnection.open() + fail("Exception expected") + } catch (ignored: IOException) { } - server.verifyConnectionClosed(); - server.verifyInteractionCompleted(); + server.verifyConnectionClosed() + server.verifyInteractionCompleted() } @Test - public void open_withIoExceptionDuringListCommand_shouldThrow() throws Exception { - settings.setAuthType(AuthType.PLAIN); - MockImapServer server = new MockImapServer(); - simplePreAuthAndLoginDialog(server, ""); - server.expect("3 LIST \"\" \"\""); - server.output("* Now what?"); - ImapConnection imapConnection = startServerAndCreateImapConnection(server); + fun `open() with IOException during LIST command should throw`() { + val server = MockImapServer().apply { + simplePreAuthAndLoginDialog() + expect("3 LIST \"\" \"\"") + output("* Now what?") + } + val imapConnection = startServerAndCreateImapConnection(server) try { - imapConnection.open(); - fail("Exception expected"); - } catch (IOException ignored) { + imapConnection.open() + fail("Exception expected") + } catch (ignored: IOException) { } - server.verifyConnectionClosed(); - server.verifyInteractionCompleted(); + server.verifyConnectionClosed() + server.verifyInteractionCompleted() } @Test - public void open_withNegativeResponseToListCommand() throws Exception { - settings.setAuthType(AuthType.PLAIN); - MockImapServer server = new MockImapServer(); - simplePreAuthAndLoginDialog(server, ""); - server.expect("3 LIST \"\" \"\""); - server.output("3 NO"); - ImapConnection imapConnection = startServerAndCreateImapConnection(server); + fun `open() with negative response to LIST command`() { + val server = MockImapServer().apply { + simplePreAuthAndLoginDialog() + expect("3 LIST \"\" \"\"") + output("3 NO") + } + val imapConnection = startServerAndCreateImapConnection(server) - imapConnection.open(); + imapConnection.open() - server.verifyConnectionStillOpen(); - server.verifyInteractionCompleted(); + server.verifyConnectionStillOpen() + server.verifyInteractionCompleted() } @Test - public void isConnected_withoutPreviousOpen_shouldReturnFalse() throws Exception { - ImapConnection imapConnection = createImapConnection(settings, socketFactory, oAuth2TokenProvider); + fun `isConnected without previous open() should return false`() { + val imapConnection = createImapConnection(settings, socketFactory, oAuth2TokenProvider) - boolean result = imapConnection.isConnected(); + val result = imapConnection.isConnected - assertFalse(result); + assertThat(result).isFalse() } @Test - public void isConnected_afterOpen_shouldReturnTrue() throws Exception { - MockImapServer server = new MockImapServer(); - ImapConnection imapConnection = simpleOpen(server); - - boolean result = imapConnection.isConnected(); + fun `isConnected after open() should return true`() { + val server = MockImapServer() + val imapConnection = simpleOpen(server) - assertTrue(result); - server.verifyConnectionStillOpen(); + val result = imapConnection.isConnected - server.shutdown(); + assertThat(result).isTrue() + server.verifyConnectionStillOpen() + server.shutdown() } @Test - public void isConnected_afterOpenAndClose_shouldReturnFalse() throws Exception { - MockImapServer server = new MockImapServer(); - ImapConnection imapConnection = simpleOpen(server); - imapConnection.close(); + fun isConnected_afterOpenAndClose_shouldReturnFalse() { + val server = MockImapServer() + val imapConnection = simpleOpen(server) + imapConnection.close() - boolean result = imapConnection.isConnected(); + val result = imapConnection.isConnected - assertFalse(result); - server.verifyConnectionClosed(); - - server.shutdown(); + assertThat(result).isFalse() + server.verifyConnectionClosed() + server.shutdown() } @Test - public void close_withoutOpen_shouldNotThrow() throws Exception { - ImapConnection imapConnection = createImapConnection(settings, socketFactory, oAuth2TokenProvider); + fun `close() without open() should not throw`() { + val imapConnection = createImapConnection(settings, socketFactory, oAuth2TokenProvider) - imapConnection.close(); + imapConnection.close() } @Test - public void close_afterOpen_shouldCloseConnection() throws Exception { - MockImapServer server = new MockImapServer(); - ImapConnection imapConnection = simpleOpen(server); - - imapConnection.close(); + fun `close() after open() should close connection`() { + val server = MockImapServer() + val imapConnection = simpleOpen(server) - server.verifyConnectionClosed(); + imapConnection.close() - server.shutdown(); + server.verifyConnectionClosed() + server.shutdown() } @Test - public void isIdleCapable_withoutIdleCapability() throws Exception { - MockImapServer server = new MockImapServer(); - ImapConnection imapConnection = simpleOpen(server); - - boolean result = imapConnection.isIdleCapable(); + fun `isIdleCapable without IDLE capability should return false`() { + val server = MockImapServer() + val imapConnection = simpleOpen(server) - assertFalse(result); + val result = imapConnection.isIdleCapable - server.shutdown(); + assertThat(result).isFalse() + server.shutdown() } @Test - public void isIdleCapable_withIdleCapability() throws Exception { - MockImapServer server = new MockImapServer(); - ImapConnection imapConnection = simpleOpenWithCapabilities(server, "IDLE"); + fun `isIdleCapable with IDLE capability should return true`() { + val server = MockImapServer() + val imapConnection = simpleOpenWithCapabilities(server, postAuthCapabilities = "IDLE") - boolean result = imapConnection.isIdleCapable(); + val result = imapConnection.isIdleCapable - assertTrue(result); - - server.shutdown(); + assertThat(result).isTrue() + server.shutdown() } @Test - public void sendContinuation() throws Exception { - settings.setAuthType(AuthType.PLAIN); - MockImapServer server = new MockImapServer(); - simpleOpenDialog(server, "IDLE"); - server.expect("4 IDLE"); - server.output("+ idling"); - server.expect("DONE"); - ImapConnection imapConnection = startServerAndCreateImapConnection(server); - imapConnection.open(); - imapConnection.sendCommand("IDLE", false); - imapConnection.readResponse(); + fun `sendContinuation() should send line without tag`() { + val server = MockImapServer().apply { + simpleOpenDialog(postAuthCapabilities = "IDLE") + expect("4 IDLE") + output("+ idling") + expect("DONE") + } + val imapConnection = startServerAndCreateImapConnection(server, authType = AuthType.PLAIN) - imapConnection.sendContinuation("DONE"); + imapConnection.open() + imapConnection.sendCommand("IDLE", false) + imapConnection.readResponse() + imapConnection.sendContinuation("DONE") - server.waitForInteractionToComplete(); - server.verifyConnectionStillOpen(); - server.verifyInteractionCompleted(); + server.waitForInteractionToComplete() + server.verifyConnectionStillOpen() + server.verifyInteractionCompleted() } @Test - public void executeSingleCommand_withOkResponse_shouldReturnResult() throws Exception { - MockImapServer server = new MockImapServer(); - simpleOpenDialog(server, ""); - server.expect("4 CREATE Folder"); - server.output("4 OK Folder created"); - ImapConnection imapConnection = startServerAndCreateImapConnection(server); + fun `executeSimpleCommand() with OK response should return result`() { + val server = MockImapServer().apply { + simpleOpenDialog() + expect("4 CREATE Folder") + output("4 OK Folder created") + } + val imapConnection = startServerAndCreateImapConnection(server) - List result = imapConnection.executeSimpleCommand("CREATE Folder"); + val result = imapConnection.executeSimpleCommand("CREATE Folder") - assertEquals(result.size(), 1); - server.verifyConnectionStillOpen(); - server.verifyInteractionCompleted(); + assertThat(result).hasSize(1) + server.verifyConnectionStillOpen() + server.verifyInteractionCompleted() } @Test - public void executeSingleCommand_withNoResponse_shouldThrowNegativeImapResponseException() throws Exception { - MockImapServer server = new MockImapServer(); - simpleOpenDialog(server, ""); - server.expect("4 CREATE Folder"); - server.output("4 NO Folder exists"); - ImapConnection imapConnection = startServerAndCreateImapConnection(server); + fun `executeSimpleCommand() with NO response should throw NegativeImapResponseException`() { + val server = MockImapServer().apply { + simpleOpenDialog() + expect("4 CREATE Folder") + output("4 NO Folder exists") + } + val imapConnection = startServerAndCreateImapConnection(server) try { - imapConnection.executeSimpleCommand("CREATE Folder"); - - fail("Expected exception"); - } catch (NegativeImapResponseException e) { - assertEquals("Folder exists", e.getLastResponse().getString(1)); + imapConnection.executeSimpleCommand("CREATE Folder") + fail("Expected exception") + } catch (e: NegativeImapResponseException) { + assertThat(e.lastResponse).containsExactly("NO", "Folder exists") } - server.verifyConnectionStillOpen(); - server.verifyInteractionCompleted(); + + server.verifyConnectionStillOpen() + server.verifyInteractionCompleted() } @Test - public void hasCapability_withNotYetOpenedConnection_shouldConnectAndFetchCapabilities() throws Exception { - MockImapServer server = new MockImapServer(); - simpleOpenDialog(server, "X-SOMETHING"); - ImapConnection imapConnection = startServerAndCreateImapConnection(server); + fun `hasCapability() with not yet opened connection should connect and fetch capabilities`() { + val server = MockImapServer().apply { + simpleOpenDialog(postAuthCapabilities = "X-SOMETHING") + } + val imapConnection = startServerAndCreateImapConnection(server) - boolean capabilityPresent = imapConnection.hasCapability("X-SOMETHING"); + val capabilityPresent = imapConnection.hasCapability("X-SOMETHING") - assertTrue(capabilityPresent); - server.verifyConnectionStillOpen(); - server.verifyInteractionCompleted(); + assertThat(capabilityPresent).isTrue() + server.verifyConnectionStillOpen() + server.verifyInteractionCompleted() } - private ImapConnection createImapConnection(ImapSettings settings, TrustedSocketFactory socketFactory, - OAuth2TokenProvider oAuth2TokenProvider) { - return new RealImapConnection(settings, socketFactory, oAuth2TokenProvider, - SOCKET_CONNECT_TIMEOUT, SOCKET_READ_TIMEOUT, 1); + private fun createImapConnection( + settings: ImapSettings, + socketFactory: TrustedSocketFactory, + oAuth2TokenProvider: OAuth2TokenProvider + ): ImapConnection { + val connectionGeneration = 1 + return RealImapConnection( + settings, + socketFactory, + oAuth2TokenProvider, + SOCKET_CONNECT_TIMEOUT, + SOCKET_READ_TIMEOUT, + connectionGeneration + ) } - private ImapConnection startServerAndCreateImapConnection(MockImapServer server) throws IOException { - server.start(); - settings.setHost(server.getHost()); - settings.setPort(server.getPort()); - return createImapConnection(settings, socketFactory, oAuth2TokenProvider); - } + private fun startServerAndCreateImapConnection( + server: MockImapServer, + authType: AuthType = AuthType.PLAIN + ): ImapConnection { + server.start() + settings.host = server.host + settings.port = server.port + settings.authType = authType - private ImapConnection simpleOpen(MockImapServer server) throws Exception { - return simpleOpenWithCapabilities(server, ""); + return createImapConnection(settings, socketFactory, oAuth2TokenProvider) } - private ImapConnection simpleOpenWithCapabilities(MockImapServer server, String postAuthCapabilities) - throws Exception { - simpleOpenDialog(server, postAuthCapabilities); - - ImapConnection imapConnection = startServerAndCreateImapConnection(server); - imapConnection.open(); - - return imapConnection; + private fun simpleOpen(server: MockImapServer): ImapConnection { + return simpleOpenWithCapabilities(server, postAuthCapabilities = "") } - private void preAuthenticationDialog(MockImapServer server) { - preAuthenticationDialog(server, ""); - } + private fun simpleOpenWithCapabilities(server: MockImapServer, postAuthCapabilities: String): ImapConnection { + server.simpleOpenDialog(postAuthCapabilities) - private void preAuthenticationDialog(MockImapServer server, String capabilities) { - server.output("* OK IMAP4rev1 Service Ready"); - server.expect("1 CAPABILITY"); - server.output("* CAPABILITY IMAP4 IMAP4REV1 " + capabilities); - server.output("1 OK CAPABILITY"); - } + val imapConnection = startServerAndCreateImapConnection(server) + imapConnection.open() - private void postAuthenticationDialogRequestingCapabilities(MockImapServer server) { - postAuthenticationDialogRequestingCapabilities(server, 3); + return imapConnection } - private void postAuthenticationDialogRequestingCapabilities(MockImapServer server, int tag) { - requestCapabilities(server, tag); - simplePostAuthenticationDialog(server, tag + 1); + private fun MockImapServer.preAuthenticationDialog(capabilities: String = "") { + output("* OK IMAP4rev1 Service Ready") + expect("1 CAPABILITY") + output("* CAPABILITY IMAP4 IMAP4REV1 $capabilities") + output("1 OK CAPABILITY") } - private void requestCapabilities(MockImapServer server, int tag) { - server.expect(tag + " CAPABILITY"); - server.output("* CAPABILITY IMAP4 IMAP4REV1 "); - server.output(tag + " OK CAPABILITY"); + private fun MockImapServer.postAuthenticationDialogRequestingCapabilities(tag: Int = 3) { + requestCapabilities(tag) + simplePostAuthenticationDialog(tag + 1) } - private void simplePostAuthenticationDialog(MockImapServer server, int tag) { - server.expect(tag + " LIST \"\" \"\""); - server.output("* LIST () \"/\" foo/bar"); - server.output(tag + " OK"); + private fun MockImapServer.requestCapabilities(tag: Int) { + expect("$tag CAPABILITY") + output("* CAPABILITY IMAP4 IMAP4REV1 ") + output("$tag OK CAPABILITY") } - private void simpleOpenDialog(MockImapServer server, String postAuthCapabilities) { - simplePreAuthAndLoginDialog(server, postAuthCapabilities); - simplePostAuthenticationDialog(server, 3); + private fun MockImapServer.simplePostAuthenticationDialog(tag: Int) { + expect("$tag LIST \"\" \"\"") + output("* LIST () \"/\" foo/bar") + output("$tag OK") } - private void simplePreAuthAndLoginDialog(MockImapServer server, String postAuthCapabilities) { - settings.setAuthType(AuthType.PLAIN); - - preAuthenticationDialog(server); - - server.expect("2 LOGIN \"" + USERNAME + "\" \"" + PASSWORD + "\""); - server.output("2 OK [CAPABILITY " + postAuthCapabilities + "] LOGIN completed"); + private fun MockImapServer.simpleOpenDialog(postAuthCapabilities: String = "") { + simplePreAuthAndLoginDialog(postAuthCapabilities) + simplePostAuthenticationDialog(3) } - private OAuth2TokenProvider createOAuth2TokenProvider() { - return new OAuth2TokenProvider() { - private int invalidationCount = 0; + private fun MockImapServer.simplePreAuthAndLoginDialog(postAuthCapabilities: String = "") { + settings.authType = AuthType.PLAIN + preAuthenticationDialog() + expect("2 LOGIN \"$USERNAME\" \"$PASSWORD\"") + output("2 OK [CAPABILITY $postAuthCapabilities] LOGIN completed") + } +} - @Override - public String getToken(String username, long timeoutMillis) throws AuthenticationFailedException { - assertEquals(USERNAME, username); - assertEquals(OAUTH2_TIMEOUT, timeoutMillis); +class TestTokenProvider : OAuth2TokenProvider { + private var invalidationCount = 0 - switch (invalidationCount) { - case 0: { - return XOAUTH_TOKEN; - } - case 1: { - return XOAUTH_ANOTHER_TOKEN; - } - default: { - throw new AuthenticationFailedException("Ran out of auth tokens. invalidateToken() called too often?"); - } - } - } + override fun getToken(username: String, timeoutMillis: Long): String { + assertThat(username).isEqualTo(USERNAME) + assertThat(timeoutMillis).isEqualTo(OAuth2TokenProvider.OAUTH2_TIMEOUT.toLong()) - @Override - public void invalidateToken(String username) { - assertEquals(USERNAME, username); - invalidationCount++; + return when (invalidationCount) { + 0 -> XOAUTH_TOKEN + 1 -> XOAUTH_TOKEN_2 + else -> { + throw AuthenticationFailedException( + "Ran out of auth tokens. invalidateToken() called too often?" + ) } + } + } - @Override - public List getAccounts() { - throw new UnsupportedOperationException(); - } + override fun invalidateToken(username: String) { + assertThat(username).isEqualTo(USERNAME) + invalidationCount++ + } - }; + override fun getAccounts(): List { + throw UnsupportedOperationException() } } + +private fun String.base64() = this.encodeUtf8().base64() -- GitLab From 35ced0f3efe197f13283be5fc86b471cd4d020ae Mon Sep 17 00:00:00 2001 From: cketti Date: Wed, 11 May 2022 01:13:11 +0200 Subject: [PATCH 49/75] Add `Logger` implementation for tests --- .../mail/store/imap/RealImapConnectionTest.kt | 3 + .../java/com/fsck/k9/mail/SystemOutLogger.kt | 70 +++++++++++++++++++ 2 files changed, 73 insertions(+) create mode 100644 mail/testing/src/main/java/com/fsck/k9/mail/SystemOutLogger.kt diff --git a/mail/protocols/imap/src/test/java/com/fsck/k9/mail/store/imap/RealImapConnectionTest.kt b/mail/protocols/imap/src/test/java/com/fsck/k9/mail/store/imap/RealImapConnectionTest.kt index c77aadbc33..4fd4ecb3fa 100644 --- a/mail/protocols/imap/src/test/java/com/fsck/k9/mail/store/imap/RealImapConnectionTest.kt +++ b/mail/protocols/imap/src/test/java/com/fsck/k9/mail/store/imap/RealImapConnectionTest.kt @@ -1,11 +1,13 @@ 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.CertificateValidationException import com.fsck.k9.mail.ConnectionSecurity import com.fsck.k9.mail.K9MailLib import com.fsck.k9.mail.MessagingException +import com.fsck.k9.mail.SystemOutLogger import com.fsck.k9.mail.XOAuth2ChallengeParserTest import com.fsck.k9.mail.helpers.TestTrustedSocketFactory import com.fsck.k9.mail.oauth.OAuth2TokenProvider @@ -43,6 +45,7 @@ class RealImapConnectionTest { @Before fun setUp() { if (DEBUGGING) { + Timber.logger = SystemOutLogger() K9MailLib.setDebug(true) K9MailLib.setDebugSensitive(true) } diff --git a/mail/testing/src/main/java/com/fsck/k9/mail/SystemOutLogger.kt b/mail/testing/src/main/java/com/fsck/k9/mail/SystemOutLogger.kt new file mode 100644 index 0000000000..d0e0c1562d --- /dev/null +++ b/mail/testing/src/main/java/com/fsck/k9/mail/SystemOutLogger.kt @@ -0,0 +1,70 @@ +package com.fsck.k9.mail + +import com.fsck.k9.logging.Logger + +class SystemOutLogger : Logger { + override fun v(message: String?, vararg args: Any?) { + System.out.printf("V/ ${message.orEmpty()}\n", *args) + } + + override fun v(t: Throwable?, message: String?, vararg args: Any?) { + t?.printStackTrace(System.out) + v(message, *args) + } + + override fun v(t: Throwable?) { + t?.printStackTrace(System.out) + } + + override fun d(message: String?, vararg args: Any?) { + System.out.printf("D/ ${message.orEmpty()}\n", *args) + } + + override fun d(t: Throwable?, message: String?, vararg args: Any?) { + t?.printStackTrace(System.out) + d(message, *args) + } + + override fun d(t: Throwable?) { + t?.printStackTrace(System.out) + } + + override fun i(message: String?, vararg args: Any?) { + System.out.printf("I/ ${message.orEmpty()}\n", *args) + } + + override fun i(t: Throwable?, message: String?, vararg args: Any?) { + t?.printStackTrace(System.out) + i(message, *args) + } + + override fun i(t: Throwable?) { + t?.printStackTrace(System.out) + } + + override fun w(message: String?, vararg args: Any?) { + System.out.printf("W/ ${message.orEmpty()}\n", *args) + } + + override fun w(t: Throwable?, message: String?, vararg args: Any?) { + t?.printStackTrace(System.out) + w(message, *args) + } + + override fun w(t: Throwable?) { + t?.printStackTrace(System.out) + } + + override fun e(message: String?, vararg args: Any?) { + System.out.printf("E/ ${message.orEmpty()}\n", *args) + } + + override fun e(t: Throwable?, message: String?, vararg args: Any?) { + t?.printStackTrace(System.out) + e(message, *args) + } + + override fun e(t: Throwable?) { + t?.printStackTrace(System.out) + } +} -- GitLab From 553cbbb87f005516270fed5394b32220adbd6702 Mon Sep 17 00:00:00 2001 From: cketti Date: Wed, 11 May 2022 01:24:44 +0200 Subject: [PATCH 50/75] Only use `XOAuth2ChallengeParser` on continuation requests --- .../k9/mail/store/imap/RealImapConnection.java | 10 ++++++---- .../mail/store/imap/RealImapConnectionTest.kt | 18 ++++++++++++++++++ 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/mail/protocols/imap/src/main/java/com/fsck/k9/mail/store/imap/RealImapConnection.java b/mail/protocols/imap/src/main/java/com/fsck/k9/mail/store/imap/RealImapConnection.java index dc8df2fb4a..b949271e87 100644 --- a/mail/protocols/imap/src/main/java/com/fsck/k9/mail/store/imap/RealImapConnection.java +++ b/mail/protocols/imap/src/main/java/com/fsck/k9/mail/store/imap/RealImapConnection.java @@ -447,14 +447,16 @@ class RealImapConnection implements ImapConnection { } private void handleXOAuthUntaggedResponse(ImapResponse response) throws IOException { + if (!response.isContinuationRequested()) { + return; + } + if (response.isString(0)) { retryXoauth2WithNewToken = XOAuth2ChallengeParser.shouldRetry(response.getString(0), settings.getHost()); } - if (response.isContinuationRequested()) { - outputStream.write("\r\n".getBytes()); - outputStream.flush(); - } + outputStream.write("\r\n".getBytes()); + outputStream.flush(); } private List authCramMD5() throws MessagingException, IOException { diff --git a/mail/protocols/imap/src/test/java/com/fsck/k9/mail/store/imap/RealImapConnectionTest.kt b/mail/protocols/imap/src/test/java/com/fsck/k9/mail/store/imap/RealImapConnectionTest.kt index 4fd4ecb3fa..eb86e91647 100644 --- a/mail/protocols/imap/src/test/java/com/fsck/k9/mail/store/imap/RealImapConnectionTest.kt +++ b/mail/protocols/imap/src/test/java/com/fsck/k9/mail/store/imap/RealImapConnectionTest.kt @@ -333,6 +333,24 @@ class RealImapConnectionTest { server.verifyInteractionCompleted() } + @Test + fun `open() AUTH XOAUTH2 with untagged CAPABILITY response after authentication`() { + val server = MockImapServer().apply { + preAuthenticationDialog(capabilities = "SASL-IR AUTH=XOAUTH AUTH=XOAUTH2") + expect("2 AUTHENTICATE XOAUTH2 $XOAUTH_STRING") + output("* CAPABILITY IMAP4rev1 X-GM-EXT-1") + output("2 OK Success") + simplePostAuthenticationDialog(tag = 3) + } + val imapConnection = startServerAndCreateImapConnection(server, authType = AuthType.XOAUTH2) + + imapConnection.open() + + assertThat(imapConnection.hasCapability("X-GM-EXT-1")).isTrue() + server.verifyConnectionStillOpen() + server.verifyInteractionCompleted() + } + @Test fun `open() AUTH XOAUTH2 throws exception on 401 response`() { val server = MockImapServer().apply { -- GitLab From bf5924d288bc221c1f2d73090a0cf8a72659de93 Mon Sep 17 00:00:00 2001 From: cketti Date: Wed, 18 May 2022 17:46:21 +0200 Subject: [PATCH 51/75] Rename .java to .kt --- .../setup/{AccountSetupBasics.java => AccountSetupBasics.kt} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename app/ui/legacy/src/main/java/com/fsck/k9/activity/setup/{AccountSetupBasics.java => AccountSetupBasics.kt} (100%) diff --git a/app/ui/legacy/src/main/java/com/fsck/k9/activity/setup/AccountSetupBasics.java b/app/ui/legacy/src/main/java/com/fsck/k9/activity/setup/AccountSetupBasics.kt similarity index 100% rename from app/ui/legacy/src/main/java/com/fsck/k9/activity/setup/AccountSetupBasics.java rename to app/ui/legacy/src/main/java/com/fsck/k9/activity/setup/AccountSetupBasics.kt -- GitLab From fa803526d2410e1615c49f4761168c8f73c171da Mon Sep 17 00:00:00 2001 From: cketti Date: Wed, 18 May 2022 17:46:21 +0200 Subject: [PATCH 52/75] Convert `AccountSetupBasics` to Kotlin --- .../k9/activity/setup/AccountSetupBasics.kt | 518 ++++++++---------- 1 file changed, 230 insertions(+), 288 deletions(-) diff --git a/app/ui/legacy/src/main/java/com/fsck/k9/activity/setup/AccountSetupBasics.kt b/app/ui/legacy/src/main/java/com/fsck/k9/activity/setup/AccountSetupBasics.kt index aa1ca7f46e..2913871e6d 100644 --- a/app/ui/legacy/src/main/java/com/fsck/k9/activity/setup/AccountSetupBasics.kt +++ b/app/ui/legacy/src/main/java/com/fsck/k9/activity/setup/AccountSetupBasics.kt @@ -1,359 +1,301 @@ -package com.fsck.k9.activity.setup; - - -import android.content.Context; -import android.content.Intent; -import android.os.Bundle; -import android.text.Editable; -import android.text.TextWatcher; -import android.view.View; -import android.view.View.OnClickListener; -import android.view.ViewGroup; -import android.widget.Button; -import android.widget.CheckBox; -import android.widget.CompoundButton; -import android.widget.CompoundButton.OnCheckedChangeListener; - -import com.fsck.k9.Account; -import com.fsck.k9.Core; -import com.fsck.k9.DI; -import com.fsck.k9.EmailAddressValidator; -import com.fsck.k9.Preferences; -import com.fsck.k9.account.AccountCreator; -import com.fsck.k9.ui.base.K9Activity; -import com.fsck.k9.activity.setup.AccountSetupCheckSettings.CheckDirection; -import com.fsck.k9.autodiscovery.api.DiscoveredServerSettings; -import com.fsck.k9.autodiscovery.api.DiscoveryResults; -import com.fsck.k9.autodiscovery.api.DiscoveryTarget; -import com.fsck.k9.autodiscovery.providersxml.ProvidersXmlDiscovery; -import com.fsck.k9.helper.Utility; -import com.fsck.k9.mail.AuthType; -import com.fsck.k9.mail.ServerSettings; -import com.fsck.k9.mailstore.SpecialLocalFoldersCreator; -import com.fsck.k9.ui.R; -import com.fsck.k9.ui.ConnectionSettings; -import com.fsck.k9.ui.settings.ExtraAccountDiscovery; -import com.fsck.k9.view.ClientCertificateSpinner; -import com.fsck.k9.view.ClientCertificateSpinner.OnClientCertificateChangedListener; -import com.google.android.material.textfield.TextInputEditText; -import timber.log.Timber; +package com.fsck.k9.activity.setup + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import android.text.Editable +import android.view.ViewGroup +import android.widget.Button +import android.widget.CheckBox +import androidx.core.view.isVisible +import com.fsck.k9.Account +import com.fsck.k9.Core +import com.fsck.k9.EmailAddressValidator +import com.fsck.k9.Preferences +import com.fsck.k9.account.AccountCreator +import com.fsck.k9.activity.setup.AccountSetupCheckSettings.CheckDirection +import com.fsck.k9.autodiscovery.api.DiscoveredServerSettings +import com.fsck.k9.autodiscovery.api.DiscoveryTarget +import com.fsck.k9.autodiscovery.providersxml.ProvidersXmlDiscovery +import com.fsck.k9.helper.SimpleTextWatcher +import com.fsck.k9.helper.Utility.requiredFieldValid +import com.fsck.k9.mail.AuthType +import com.fsck.k9.mail.ServerSettings +import com.fsck.k9.mailstore.SpecialLocalFoldersCreator +import com.fsck.k9.ui.ConnectionSettings +import com.fsck.k9.ui.R +import com.fsck.k9.ui.base.K9Activity +import com.fsck.k9.ui.settings.ExtraAccountDiscovery +import com.fsck.k9.view.ClientCertificateSpinner +import com.google.android.material.textfield.TextInputEditText +import org.koin.android.ext.android.inject /** * Prompts the user for the email address and password. - * Attempts to lookup default settings for the domain the user specified. If the - * domain is known the settings are handed off to the AccountSetupCheckSettings - * activity. If no settings are found the settings are handed off to the - * AccountSetupAccountType activity. + * + * Attempts to lookup default settings for the domain the user specified. If the domain is known, the settings are + * handed off to the [AccountSetupCheckSettings] activity. If no settings are found, the settings are handed off to the + * [AccountSetupAccountType] activity. */ -public class AccountSetupBasics extends K9Activity - implements OnClickListener, TextWatcher, OnCheckedChangeListener, OnClientCertificateChangedListener { - private final static String EXTRA_ACCOUNT = "com.fsck.k9.AccountSetupBasics.account"; - private final static String STATE_KEY_CHECKED_INCOMING = "com.fsck.k9.AccountSetupBasics.checkedIncoming"; - - - private final ProvidersXmlDiscovery providersXmlDiscovery = DI.get(ProvidersXmlDiscovery.class); - private final AccountCreator accountCreator = DI.get(AccountCreator.class); - private final SpecialLocalFoldersCreator localFoldersCreator = DI.get(SpecialLocalFoldersCreator.class); - - private TextInputEditText mEmailView; - private TextInputEditText mPasswordView; - private CheckBox mClientCertificateCheckBox; - private ClientCertificateSpinner mClientCertificateSpinner; - private Button mNextButton; - private Button mManualSetupButton; - private Account mAccount; - private ViewGroup mAllowClientCertificateView; - - private EmailAddressValidator mEmailValidator = new EmailAddressValidator(); - private boolean mCheckedIncoming = false; - - public static void actionNewAccount(Context context) { - Intent i = new Intent(context, AccountSetupBasics.class); - context.startActivity(i); +class AccountSetupBasics : K9Activity() { + private val providersXmlDiscovery: ProvidersXmlDiscovery by inject() + private val accountCreator: AccountCreator by inject() + private val localFoldersCreator: SpecialLocalFoldersCreator by inject() + private val preferences: Preferences by inject() + private val emailValidator: EmailAddressValidator by inject() + + private lateinit var emailView: TextInputEditText + private lateinit var passwordView: TextInputEditText + private lateinit var clientCertificateCheckBox: CheckBox + private lateinit var clientCertificateSpinner: ClientCertificateSpinner + private lateinit var nextButton: Button + private lateinit var manualSetupButton: Button + private lateinit var allowClientCertificateView: ViewGroup + + private var account: Account? = null + private var checkedIncoming = false + + public override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setLayout(R.layout.account_setup_basics) + setTitle(R.string.account_setup_basics_title) + + emailView = findViewById(R.id.account_email) + passwordView = findViewById(R.id.account_password) + clientCertificateCheckBox = findViewById(R.id.account_client_certificate) + clientCertificateSpinner = findViewById(R.id.account_client_certificate_spinner) + allowClientCertificateView = findViewById(R.id.account_allow_client_certificate) + nextButton = findViewById(R.id.next) + manualSetupButton = findViewById(R.id.manual_setup) + + manualSetupButton.setOnClickListener { onManualSetup() } + nextButton.setOnClickListener { onNext() } } - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setLayout(R.layout.account_setup_basics); - setTitle(R.string.account_setup_basics_title); - mEmailView = findViewById(R.id.account_email); - mPasswordView = findViewById(R.id.account_password); - mClientCertificateCheckBox = findViewById(R.id.account_client_certificate); - mClientCertificateSpinner = findViewById(R.id.account_client_certificate_spinner); - mAllowClientCertificateView = findViewById(R.id.account_allow_client_certificate); - - mNextButton = findViewById(R.id.next); - mManualSetupButton = findViewById(R.id.manual_setup); - mNextButton.setOnClickListener(this); - mManualSetupButton.setOnClickListener(this); - } + override fun onPostCreate(savedInstanceState: Bundle?) { + super.onPostCreate(savedInstanceState) - private void initializeViewListeners() { - mEmailView.addTextChangedListener(this); - mPasswordView.addTextChangedListener(this); - mClientCertificateCheckBox.setOnCheckedChangeListener(this); - mClientCertificateSpinner.setOnClientCertificateChangedListener(this); + /* + * We wait until now to initialize the listeners because we didn't want the OnCheckedChangeListener active + * while the clientCertificateCheckBox state was being restored because it could trigger the pop-up of a + * ClientCertificateSpinner.chooseCertificate() dialog. + */ + initializeViewListeners() + validateFields() } - @Override - public void onSaveInstanceState(Bundle outState) { - super.onSaveInstanceState(outState); - if (mAccount != null) { - outState.putString(EXTRA_ACCOUNT, mAccount.getUuid()); + private fun initializeViewListeners() { + val textWatcher = object : SimpleTextWatcher() { + override fun afterTextChanged(s: Editable) { + validateFields() + } } - outState.putBoolean(STATE_KEY_CHECKED_INCOMING, mCheckedIncoming); - } - @Override - protected void onRestoreInstanceState(Bundle savedInstanceState) { - super.onRestoreInstanceState(savedInstanceState); + emailView.addTextChangedListener(textWatcher) + passwordView.addTextChangedListener(textWatcher) - if (savedInstanceState.containsKey(EXTRA_ACCOUNT)) { - String accountUuid = savedInstanceState.getString(EXTRA_ACCOUNT); - mAccount = Preferences.getPreferences(this).getAccount(accountUuid); - } + clientCertificateCheckBox.setOnCheckedChangeListener { _, isChecked -> + updateViewVisibility(isChecked) + validateFields() - mCheckedIncoming = savedInstanceState.getBoolean(STATE_KEY_CHECKED_INCOMING); + // Have the user select the client certificate if not already selected + if (isChecked && clientCertificateSpinner.alias == null) { + clientCertificateSpinner.chooseCertificate() + } + } - updateViewVisibility(mClientCertificateCheckBox.isChecked()); + clientCertificateSpinner.setOnClientCertificateChangedListener { + validateFields() + } } - @Override - protected void onPostCreate(Bundle savedInstanceState) { - super.onPostCreate(savedInstanceState); + override fun onSaveInstanceState(outState: Bundle) { + super.onSaveInstanceState(outState) - /* - * We wait until now to initialize the listeners because we didn't want - * the OnCheckedChangeListener active while the - * mClientCertificateCheckBox state was being restored because it could - * trigger the pop-up of a ClientCertificateSpinner.chooseCertificate() - * dialog. - */ - initializeViewListeners(); - validateFields(); + outState.putString(EXTRA_ACCOUNT, account?.uuid) + outState.putBoolean(STATE_KEY_CHECKED_INCOMING, checkedIncoming) } - public void afterTextChanged(Editable s) { - validateFields(); - } + override fun onRestoreInstanceState(savedInstanceState: Bundle) { + super.onRestoreInstanceState(savedInstanceState) - public void beforeTextChanged(CharSequence s, int start, int count, int after) { - } + val accountUuid = savedInstanceState.getString(EXTRA_ACCOUNT) + if (accountUuid != null) { + account = preferences.getAccount(accountUuid) + } - public void onTextChanged(CharSequence s, int start, int before, int count) { + checkedIncoming = savedInstanceState.getBoolean(STATE_KEY_CHECKED_INCOMING) + updateViewVisibility(clientCertificateCheckBox.isChecked) } - @Override - public void onClientCertificateChanged(String alias) { - validateFields(); + private fun updateViewVisibility(usingCertificates: Boolean) { + allowClientCertificateView.isVisible = usingCertificates } - /** - * Called when checking the client certificate CheckBox - */ - @Override - public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { - updateViewVisibility(isChecked); - validateFields(); - - // Have the user select the client certificate if not already selected - if ((isChecked) && (mClientCertificateSpinner.getAlias() == null)) { - mClientCertificateSpinner.chooseCertificate(); - } - } + private fun validateFields() { + val email = emailView.text?.toString().orEmpty() + val valid = requiredFieldValid(emailView) && emailValidator.isValidAddressOnly(email) && isPasswordFieldValid() - private void updateViewVisibility(boolean usingCertificates) { - if (usingCertificates) { - // show client certificate spinner - mAllowClientCertificateView.setVisibility(View.VISIBLE); - } else { - // hide client certificate spinner - mAllowClientCertificateView.setVisibility(View.GONE); - } + nextButton.isEnabled = valid + nextButton.isFocusable = valid + manualSetupButton.isEnabled = valid } - private void validateFields() { - boolean clientCertificateChecked = mClientCertificateCheckBox.isChecked(); - String clientCertificateAlias = mClientCertificateSpinner.getAlias(); - String email = mEmailView.getText().toString(); + private fun isPasswordFieldValid(): Boolean { + val clientCertificateChecked = clientCertificateCheckBox.isChecked + val clientCertificateAlias = clientCertificateSpinner.alias - boolean valid = Utility.requiredFieldValid(mEmailView) - && ((!clientCertificateChecked && Utility.requiredFieldValid(mPasswordView)) - || (clientCertificateChecked && clientCertificateAlias != null)) - && mEmailValidator.isValidAddressOnly(email); - - mNextButton.setEnabled(valid); - mNextButton.setFocusable(valid); - mManualSetupButton.setEnabled(valid); - /* - * Dim the next button's icon to 50% if the button is disabled. - * TODO this can probably be done with a stateful drawable. Check into it. - * android:state_enabled - */ - Utility.setCompoundDrawablesAlpha(mNextButton, mNextButton.isEnabled() ? 255 : 128); + return !clientCertificateChecked && requiredFieldValid(passwordView) || + clientCertificateChecked && clientCertificateAlias != null } - private String getOwnerName() { - String name = null; - try { - name = getDefaultSenderName(); - } catch (Exception e) { - Timber.e(e, "Could not get default account name"); + private fun onNext() { + if (clientCertificateCheckBox.isChecked) { + // Auto-setup doesn't support client certificates. + onManualSetup() + return } - if (name == null) { - name = ""; + val email = emailView.text?.toString() ?: error("Email missing") + + val extraConnectionSettings = ExtraAccountDiscovery.discover(email) + if (extraConnectionSettings != null) { + finishAutoSetup(extraConnectionSettings) + return } - return name; - } - private String getDefaultSenderName() { - String name = null; - Account account = Preferences.getPreferences(this).getDefaultAccount(); - if (account != null) { - name = account.getSenderName(); + val connectionSettings = providersXmlDiscoveryDiscover(email) + if (connectionSettings != null) { + finishAutoSetup(connectionSettings) + } else { + // We don't have default settings for this account, start the manual setup process. + onManualSetup() } - return name; } - private void finishAutoSetup(ConnectionSettings connectionSettings) { - String email = mEmailView.getText().toString(); - String password = mPasswordView.getText().toString(); + private fun finishAutoSetup(connectionSettings: ConnectionSettings) { + val email = emailView.text?.toString() ?: error("Email missing") + val password = passwordView.text?.toString() - if (mAccount == null) { - mAccount = Preferences.getPreferences(this).newAccount(); - mAccount.setChipColor(accountCreator.pickColor()); - } - - mAccount.setSenderName(getOwnerName()); - mAccount.setEmail(email); + val account = initAccount(email) - ServerSettings incomingServerSettings = connectionSettings.getIncoming().newPassword(password); - mAccount.setIncomingServerSettings(incomingServerSettings); + val incomingServerSettings = connectionSettings.incoming.newPassword(password) + account.incomingServerSettings = incomingServerSettings - ServerSettings outgoingServerSettings = connectionSettings.getOutgoing().newPassword(password); - mAccount.setOutgoingServerSettings(outgoingServerSettings); + val outgoingServerSettings = connectionSettings.outgoing.newPassword(password) + account.outgoingServerSettings = outgoingServerSettings - mAccount.setDeletePolicy(accountCreator.getDefaultDeletePolicy(incomingServerSettings.type)); + account.deletePolicy = accountCreator.getDefaultDeletePolicy(incomingServerSettings.type) - localFoldersCreator.createSpecialLocalFolders(mAccount); + localFoldersCreator.createSpecialLocalFolders(account) - // Check incoming here. Then check outgoing in onActivityResult() - AccountSetupCheckSettings.actionCheckSettings(this, mAccount, CheckDirection.INCOMING); + // Check incoming here. Then check outgoing in onActivityResult() + AccountSetupCheckSettings.actionCheckSettings(this, account, CheckDirection.INCOMING) } - private ConnectionSettings providersXmlDiscoveryDiscover(String email, DiscoveryTarget discoveryTarget) { - DiscoveryResults discoveryResults = providersXmlDiscovery.discover(email, DiscoveryTarget.INCOMING_AND_OUTGOING); - if (discoveryResults == null || (discoveryResults.getIncoming().size() < 1 || discoveryResults.getOutgoing().size() < 1)) { - return null; + private fun onManualSetup() { + val email = emailView.text?.toString() ?: error("Email missing") + var password: String? = passwordView.text?.toString() + var clientCertificateAlias: String? = null + var authenticationType: AuthType = AuthType.PLAIN + + if (clientCertificateCheckBox.isChecked) { + clientCertificateAlias = clientCertificateSpinner.alias + if (password.isNullOrEmpty()) { + authenticationType = AuthType.EXTERNAL + password = null + } } - DiscoveredServerSettings incoming = discoveryResults.getIncoming().get(0); - DiscoveredServerSettings outgoing = discoveryResults.getOutgoing().get(0); - return new ConnectionSettings(new ServerSettings( - incoming.getProtocol(), - incoming.getHost(), - incoming.getPort(), - incoming.getSecurity(), - incoming.getAuthType(), - incoming.getUsername(), - null, - null - ), new ServerSettings( - outgoing.getProtocol(), - outgoing.getHost(), - outgoing.getPort(), - outgoing.getSecurity(), - outgoing.getAuthType(), - outgoing.getUsername(), - null, - null - )); + + val account = initAccount(email) + + val initialAccountSettings = InitialAccountSettings( + authenticationType = authenticationType, + email = email, + password = password, + clientCertificateAlias = clientCertificateAlias + ) + + AccountSetupAccountType.actionSelectAccountType(this, account, makeDefault = false, initialAccountSettings) } - private void onNext() { - if (mClientCertificateCheckBox.isChecked()) { + private fun initAccount(email: String): Account { + val account = this.account ?: createAccount().also { this.account = it } - // Auto-setup doesn't support client certificates. - onManualSetup(); - return; + account.senderName = getOwnerName() + account.email = email + return account + } + + private fun createAccount(): Account { + return preferences.newAccount().apply { + chipColor = accountCreator.pickColor() } + } - String email = mEmailView.getText().toString(); + private fun getOwnerName(): String { + return preferences.defaultAccount?.senderName ?: "" + } - ConnectionSettings extraConnectionSettings = ExtraAccountDiscovery.discover(email); - if (extraConnectionSettings != null) { - finishAutoSetup(extraConnectionSettings); - return; + private fun providersXmlDiscoveryDiscover(email: String): ConnectionSettings? { + val discoveryResults = providersXmlDiscovery.discover(email, DiscoveryTarget.INCOMING_AND_OUTGOING) + if (discoveryResults == null || discoveryResults.incoming.isEmpty() || discoveryResults.outgoing.isEmpty()) { + return null } - ConnectionSettings connectionSettings = providersXmlDiscoveryDiscover(email, DiscoveryTarget.INCOMING_AND_OUTGOING); - if (connectionSettings != null) { - finishAutoSetup(connectionSettings); - } else { - // We don't have default settings for this account, start the manual setup process. - onManualSetup(); - } + val incomingServerSettings = discoveryResults.incoming.first().toServerSettings() ?: return null + val outgoingServerSettings = discoveryResults.outgoing.first().toServerSettings() ?: return null + + return ConnectionSettings(incomingServerSettings, outgoingServerSettings) } - @Override - public void onActivityResult(int requestCode, int resultCode, Intent data) { + public override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { if (requestCode != AccountSetupCheckSettings.ACTIVITY_REQUEST_CODE) { - super.onActivityResult(requestCode, resultCode, data); - return; + super.onActivityResult(requestCode, resultCode, data) + return } if (resultCode == RESULT_OK) { - if (!mCheckedIncoming) { - //We've successfully checked incoming. Now check outgoing. - mCheckedIncoming = true; - AccountSetupCheckSettings.actionCheckSettings(this, mAccount, CheckDirection.OUTGOING); + if (!checkedIncoming) { + // We've successfully checked incoming. Now check outgoing. + checkedIncoming = true + AccountSetupCheckSettings.actionCheckSettings(this, account, CheckDirection.OUTGOING) } else { - //We've successfully checked outgoing as well. - mAccount.setName(mAccount.getEmail()); - Preferences.getPreferences(this).saveAccount(mAccount); - Core.setServicesEnabled(this); - AccountSetupNames.actionSetNames(this, mAccount); - } - } - } - - private void onManualSetup() { - String email = mEmailView.getText().toString(); + // We've successfully checked outgoing as well. + val account = this.account ?: error("Account instance missing") - String password = null; - String clientCertificateAlias = null; - AuthType authenticationType; + preferences.saveAccount(account) + Core.setServicesEnabled(applicationContext) - authenticationType = AuthType.PLAIN; - password = mPasswordView.getText().toString(); - if (mClientCertificateCheckBox.isChecked()) { - clientCertificateAlias = mClientCertificateSpinner.getAlias(); - if (mPasswordView.getText().toString().equals("")) { - authenticationType = AuthType.EXTERNAL; - password = null; + AccountSetupNames.actionSetNames(this, account) } } - - if (mAccount == null) { - mAccount = Preferences.getPreferences(this).newAccount(); - mAccount.setChipColor(accountCreator.pickColor()); - } - mAccount.setSenderName(getOwnerName()); - mAccount.setEmail(email); - - InitialAccountSettings initialAccountSettings = new InitialAccountSettings(authenticationType, email, password, - clientCertificateAlias); - - AccountSetupAccountType.actionSelectAccountType(this, mAccount, false, initialAccountSettings); } - public void onClick(View v) { - int id = v.getId(); - if (id == R.id.next) { - onNext(); - } else if (id == R.id.manual_setup) { - onManualSetup(); + companion object { + private const val EXTRA_ACCOUNT = "com.fsck.k9.AccountSetupBasics.account" + private const val STATE_KEY_CHECKED_INCOMING = "com.fsck.k9.AccountSetupBasics.checkedIncoming" + + @JvmStatic + fun actionNewAccount(context: Context) { + val intent = Intent(context, AccountSetupBasics::class.java) + context.startActivity(intent) } } } + +private fun DiscoveredServerSettings.toServerSettings(): ServerSettings? { + val authType = this.authType ?: return null + val username = this.username ?: return null + + return ServerSettings( + type = protocol, + host = host, + port = port, + connectionSecurity = security, + authenticationType = authType, + username = username, + password = null, + clientCertificateAlias = null + ) +} -- GitLab From af2d031385cd0aa03cd0e675d8728b937e51d2bb Mon Sep 17 00:00:00 2001 From: cketti Date: Sat, 21 May 2022 19:40:54 +0200 Subject: [PATCH 53/75] Rename .java to .kt --- ...ccountSetupCheckSettings.java => AccountSetupCheckSettings.kt} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename app/ui/legacy/src/main/java/com/fsck/k9/activity/setup/{AccountSetupCheckSettings.java => AccountSetupCheckSettings.kt} (100%) diff --git a/app/ui/legacy/src/main/java/com/fsck/k9/activity/setup/AccountSetupCheckSettings.java b/app/ui/legacy/src/main/java/com/fsck/k9/activity/setup/AccountSetupCheckSettings.kt similarity index 100% rename from app/ui/legacy/src/main/java/com/fsck/k9/activity/setup/AccountSetupCheckSettings.java rename to app/ui/legacy/src/main/java/com/fsck/k9/activity/setup/AccountSetupCheckSettings.kt -- GitLab From f71615f6aa0cf8de6f54c9deaf49d0ad2d914a5c Mon Sep 17 00:00:00 2001 From: cketti Date: Sat, 21 May 2022 19:40:54 +0200 Subject: [PATCH 54/75] Convert `AccountSetupCheckSettings` to Kotlin --- .../k9/activity/setup/AccountSetupBasics.kt | 4 +- .../setup/AccountSetupCheckSettings.kt | 724 ++++++++---------- 2 files changed, 332 insertions(+), 396 deletions(-) diff --git a/app/ui/legacy/src/main/java/com/fsck/k9/activity/setup/AccountSetupBasics.kt b/app/ui/legacy/src/main/java/com/fsck/k9/activity/setup/AccountSetupBasics.kt index 2913871e6d..41d2c0df8d 100644 --- a/app/ui/legacy/src/main/java/com/fsck/k9/activity/setup/AccountSetupBasics.kt +++ b/app/ui/legacy/src/main/java/com/fsck/k9/activity/setup/AccountSetupBasics.kt @@ -256,14 +256,14 @@ class AccountSetupBasics : K9Activity() { } if (resultCode == RESULT_OK) { + val account = this.account ?: error("Account instance missing") + if (!checkedIncoming) { // We've successfully checked incoming. Now check outgoing. checkedIncoming = true AccountSetupCheckSettings.actionCheckSettings(this, account, CheckDirection.OUTGOING) } else { // We've successfully checked outgoing as well. - val account = this.account ?: error("Account instance missing") - preferences.saveAccount(account) Core.setServicesEnabled(applicationContext) diff --git a/app/ui/legacy/src/main/java/com/fsck/k9/activity/setup/AccountSetupCheckSettings.kt b/app/ui/legacy/src/main/java/com/fsck/k9/activity/setup/AccountSetupCheckSettings.kt index 450eb29318..dd2b71c4de 100644 --- a/app/ui/legacy/src/main/java/com/fsck/k9/activity/setup/AccountSetupCheckSettings.kt +++ b/app/ui/legacy/src/main/java/com/fsck/k9/activity/setup/AccountSetupCheckSettings.kt @@ -1,408 +1,336 @@ - -package com.fsck.k9.activity.setup; - - -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; -import java.security.cert.CertificateEncodingException; -import java.security.cert.CertificateException; -import java.security.cert.X509Certificate; -import java.util.Collection; -import java.util.List; -import java.util.Locale; - -import android.app.Activity; -import android.app.AlertDialog; -import android.content.DialogInterface; -import android.content.Intent; -import android.os.AsyncTask; -import android.os.Bundle; -import android.os.Handler; -import android.view.View; -import android.view.View.OnClickListener; -import android.widget.ProgressBar; -import android.widget.TextView; - -import androidx.fragment.app.DialogFragment; -import androidx.fragment.app.FragmentTransaction; -import com.fsck.k9.Account; -import com.fsck.k9.DI; -import com.fsck.k9.LocalKeyStoreManager; -import com.fsck.k9.Preferences; -import com.fsck.k9.controller.MessagingController; -import com.fsck.k9.fragment.ConfirmationDialogFragment; -import com.fsck.k9.fragment.ConfirmationDialogFragment.ConfirmationDialogFragmentListener; -import com.fsck.k9.mail.AuthenticationFailedException; -import com.fsck.k9.mail.CertificateValidationException; -import com.fsck.k9.mail.MailServerDirection; -import com.fsck.k9.mail.MessagingException; -import com.fsck.k9.mail.filter.Hex; -import com.fsck.k9.preferences.Protocols; -import com.fsck.k9.ui.R; -import com.fsck.k9.ui.base.K9Activity; -import timber.log.Timber; - +package com.fsck.k9.activity.setup + +import android.app.Activity +import android.app.AlertDialog +import android.content.Intent +import android.os.AsyncTask +import android.os.Bundle +import android.os.Handler +import android.os.Looper +import android.view.View +import android.widget.ProgressBar +import android.widget.TextView +import androidx.fragment.app.DialogFragment +import androidx.fragment.app.commit +import com.fsck.k9.Account +import com.fsck.k9.LocalKeyStoreManager +import com.fsck.k9.Preferences +import com.fsck.k9.controller.MessagingController +import com.fsck.k9.fragment.ConfirmationDialogFragment +import com.fsck.k9.fragment.ConfirmationDialogFragment.ConfirmationDialogFragmentListener +import com.fsck.k9.mail.AuthenticationFailedException +import com.fsck.k9.mail.CertificateValidationException +import com.fsck.k9.mail.MailServerDirection +import com.fsck.k9.mail.filter.Hex +import com.fsck.k9.preferences.Protocols +import com.fsck.k9.ui.R +import com.fsck.k9.ui.base.K9Activity +import java.security.MessageDigest +import java.security.NoSuchAlgorithmException +import java.security.cert.CertificateEncodingException +import java.security.cert.CertificateException +import java.security.cert.X509Certificate +import java.util.Locale +import org.koin.android.ext.android.inject +import timber.log.Timber /** - * Checks the given settings to make sure that they can be used to send and - * receive mail. - * - * XXX NOTE: The manifest for this app has it ignore config changes, because - * it doesn't correctly deal with restarting while its thread is running. + * Checks the given settings to make sure that they can be used to send and receive mail. + * + * XXX NOTE: The manifest for this app has it ignore config changes, because it doesn't correctly deal with restarting + * while its thread is running. */ -public class AccountSetupCheckSettings extends K9Activity implements OnClickListener, - ConfirmationDialogFragmentListener{ - - public static final int ACTIVITY_REQUEST_CODE = 1; - - private static final String EXTRA_ACCOUNT = "account"; - - private static final String EXTRA_CHECK_DIRECTION ="checkDirection"; +class AccountSetupCheckSettings : K9Activity(), ConfirmationDialogFragmentListener { + private val messagingController: MessagingController by inject() + private val preferences: Preferences by inject() + private val localKeyStoreManager: LocalKeyStoreManager by inject() - public enum CheckDirection { - INCOMING, - OUTGOING; - - public MailServerDirection toMailServerDirection() { - switch (this) { - case INCOMING: return MailServerDirection.INCOMING; - case OUTGOING: return MailServerDirection.OUTGOING; - } - - throw new AssertionError("Unknown value: " + this); - } - } + private val handler = Handler(Looper.myLooper()!!) - private final MessagingController messagingController = DI.get(MessagingController.class); + private lateinit var progressBar: ProgressBar + private lateinit var messageView: TextView - private Handler mHandler = new Handler(); + private lateinit var account: Account + private lateinit var direction: CheckDirection - private ProgressBar mProgressBar; + @Volatile + private var canceled = false - private TextView mMessageView; + @Volatile + private var destroyed = false - private Account mAccount; + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setLayout(R.layout.account_setup_check_settings) - private CheckDirection mDirection; + messageView = findViewById(R.id.message) + progressBar = findViewById(R.id.progress) + findViewById(R.id.cancel).setOnClickListener { onCancel() } - private boolean mCanceled; + setMessage(R.string.account_setup_check_settings_retr_info_msg) + progressBar.isIndeterminate = true - private boolean mDestroyed; + val accountUuid = intent.getStringExtra(EXTRA_ACCOUNT) ?: error("Missing account UUID") + account = preferences.getAccount(accountUuid) ?: error("Could not find account") + direction = intent.getSerializableExtra(EXTRA_CHECK_DIRECTION) as CheckDirection? + ?: error("Missing CheckDirection") - public static void actionCheckSettings(Activity context, Account account, - CheckDirection direction) { - Intent i = new Intent(context, AccountSetupCheckSettings.class); - i.putExtra(EXTRA_ACCOUNT, account.getUuid()); - i.putExtra(EXTRA_CHECK_DIRECTION, direction); - context.startActivityForResult(i, ACTIVITY_REQUEST_CODE); + CheckAccountTask(account).execute(direction) } - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setLayout(R.layout.account_setup_check_settings); - mMessageView = findViewById(R.id.message); - mProgressBar = findViewById(R.id.progress); - findViewById(R.id.cancel).setOnClickListener(this); + private fun handleCertificateValidationException(exception: CertificateValidationException) { + Timber.e(exception, "Error while testing settings") - setMessage(R.string.account_setup_check_settings_retr_info_msg); - mProgressBar.setIndeterminate(true); + val chain = exception.certChain - String accountUuid = getIntent().getStringExtra(EXTRA_ACCOUNT); - mAccount = Preferences.getPreferences(this).getAccount(accountUuid); - mDirection = (CheckDirection) getIntent().getSerializableExtra(EXTRA_CHECK_DIRECTION); - - new CheckAccountTask(mAccount).execute(mDirection); - } - - private void handleCertificateValidationException(CertificateValidationException cve) { - Timber.e(cve, "Error while testing settings"); - - X509Certificate[] chain = cve.getCertChain(); // Avoid NullPointerException in acceptKeyDialog() if (chain != null) { acceptKeyDialog( - R.string.account_setup_failed_dlg_certificate_message_fmt, - cve); + R.string.account_setup_failed_dlg_certificate_message_fmt, + exception + ) } else { showErrorDialog( - R.string.account_setup_failed_dlg_server_message_fmt, - errorMessageForCertificateException(cve)); + R.string.account_setup_failed_dlg_server_message_fmt, + errorMessageForCertificateException(exception)!! + ) } } + override fun onDestroy() { + super.onDestroy() - @Override - public void onDestroy() { - super.onDestroy(); - mDestroyed = true; - mCanceled = true; + destroyed = true + canceled = true } - private void setMessage(final int resId) { - mMessageView.setText(getString(resId)); + private fun setMessage(resId: Int) { + messageView.text = getString(resId) } - private void acceptKeyDialog(final int msgResId, final CertificateValidationException ex) { - mHandler.post(new Runnable() { - public void run() { - if (mDestroyed) { - return; - } - String exMessage = "Unknown Error"; - - if (ex != null) { - if (ex.getCause() != null) { - if (ex.getCause().getCause() != null) { - exMessage = ex.getCause().getCause().getMessage(); - - } else { - exMessage = ex.getCause().getMessage(); - } - } else { - exMessage = ex.getMessage(); - } - } + private fun acceptKeyDialog(msgResId: Int, exception: CertificateValidationException) { + handler.post { + if (destroyed) { + return@post + } - mProgressBar.setIndeterminate(false); - StringBuilder chainInfo = new StringBuilder(200); - final X509Certificate[] chain = ex.getCertChain(); - // We already know chain != null (tested before calling this method) - for (int i = 0; i < chain.length; i++) { - // display certificate chain information - //TODO: localize this strings - chainInfo.append("Certificate chain[").append(i).append("]:\n"); - chainInfo.append("Subject: ").append(chain[i].getSubjectDN().toString()).append("\n"); - - // display SubjectAltNames too - // (the user may be mislead into mistrusting a certificate - // by a subjectDN not matching the server even though a - // SubjectAltName matches) - try { - final Collection < List> subjectAlternativeNames = chain[i].getSubjectAlternativeNames(); - if (subjectAlternativeNames != null) { - // The list of SubjectAltNames may be very long - //TODO: localize this string - StringBuilder altNamesText = new StringBuilder(); - altNamesText.append("Subject has ").append(subjectAlternativeNames.size()).append(" alternative names\n"); - - // we need these for matching - String incomingServerHost = mAccount.getIncomingServerSettings().host; - String outgoingServerHost = mAccount.getOutgoingServerSettings().host; - - for (List subjectAlternativeName : subjectAlternativeNames) { - Integer type = (Integer)subjectAlternativeName.get(0); - Object value = subjectAlternativeName.get(1); - String name; - switch (type) { - case 0: - Timber.w("SubjectAltName of type OtherName not supported."); - continue; - case 1: // RFC822Name - name = (String)value; - break; - case 2: // DNSName - name = (String)value; - break; - case 3: - Timber.w("unsupported SubjectAltName of type x400Address"); - continue; - case 4: - Timber.w("unsupported SubjectAltName of type directoryName"); - continue; - case 5: - Timber.w("unsupported SubjectAltName of type ediPartyName"); - continue; - case 6: // Uri - name = (String)value; - break; - case 7: // ip-address - name = (String)value; - break; - default: - Timber.w("unsupported SubjectAltName of unknown type"); - continue; + val errorMessage = exception.cause?.cause?.message ?: exception.cause?.message ?: exception.message + + progressBar.isIndeterminate = false + + val chainInfo = StringBuilder() + val chain = exception.certChain + + // We already know chain != null (tested before calling this method) + for (i in chain.indices) { + // display certificate chain information + // TODO: localize this strings + chainInfo.append("Certificate chain[").append(i).append("]:\n") + chainInfo.append("Subject: ").append(chain[i].subjectDN.toString()).append("\n") + + // display SubjectAltNames too + // (the user may be mislead into mistrusting a certificate + // by a subjectDN not matching the server even though a + // SubjectAltName matches) + try { + val subjectAlternativeNames = chain[i].subjectAlternativeNames + if (subjectAlternativeNames != null) { + // TODO: localize this string + val altNamesText = StringBuilder() + altNamesText.append("Subject has ") + .append(subjectAlternativeNames.size) + .append(" alternative names\n") + + // we need these for matching + val incomingServerHost = account.incomingServerSettings.host!! + val outgoingServerHost = account.outgoingServerSettings.host!! + for (subjectAlternativeName in subjectAlternativeNames) { + val type = subjectAlternativeName[0] as Int + val value: Any? = subjectAlternativeName[1] + val name: String = when (type) { + 0 -> { + Timber.w("SubjectAltName of type OtherName not supported.") + continue } - - // if some of the SubjectAltNames match the store or transport -host, - // display them - if (name.equalsIgnoreCase(incomingServerHost) || name.equalsIgnoreCase(outgoingServerHost)) { - //TODO: localize this string - altNamesText.append("Subject(alt): ").append(name).append(",...\n"); - } else if (name.startsWith("*.") && ( - incomingServerHost.endsWith(name.substring(2)) || - outgoingServerHost.endsWith(name.substring(2)))) { - //TODO: localize this string - altNamesText.append("Subject(alt): ").append(name).append(",...\n"); + 1 -> value as String + 2 -> value as String + 3 -> { + Timber.w("unsupported SubjectAltName of type x400Address") + continue + } + 4 -> { + Timber.w("unsupported SubjectAltName of type directoryName") + continue + } + 5 -> { + Timber.w("unsupported SubjectAltName of type ediPartyName") + continue } + 6 -> value as String + 7 -> value as String + else -> { + Timber.w("unsupported SubjectAltName of unknown type") + continue + } + } + + // if some of the SubjectAltNames match the store or transport -host, display them + if (name.equals(incomingServerHost, ignoreCase = true) || + name.equals(outgoingServerHost, ignoreCase = true) + ) { + // TODO: localize this string + altNamesText.append("Subject(alt): ").append(name).append(",...\n") + } else if (name.startsWith("*.") && + ( + incomingServerHost.endsWith(name.substring(2)) || + outgoingServerHost.endsWith(name.substring(2)) + ) + ) { + // TODO: localize this string + altNamesText.append("Subject(alt): ").append(name).append(",...\n") } - chainInfo.append(altNamesText); } - } catch (Exception e1) { - // don't fail just because of subjectAltNames - Timber.w(e1, "cannot display SubjectAltNames in dialog"); + chainInfo.append(altNamesText) } + } catch (e: Exception) { + // don't fail just because of subjectAltNames + Timber.w(e, "cannot display SubjectAltNames in dialog") + } - chainInfo.append("Issuer: ").append(chain[i].getIssuerDN().toString()).append("\n"); - String[] digestAlgorithms = new String[] {"SHA-1", "SHA-256", "SHA-512"}; + chainInfo.append("Issuer: ").append(chain[i].issuerDN.toString()).append("\n") + for (algorithm in arrayOf("SHA-1", "SHA-256", "SHA-512")) { + val digest = try { + MessageDigest.getInstance(algorithm) + } catch (e: NoSuchAlgorithmException) { + Timber.e(e, "Error while initializing MessageDigest ($algorithm)") + null + } - for (String algorithm : digestAlgorithms) { - MessageDigest digest = null; + if (digest != null) { + digest.reset() try { - digest = MessageDigest.getInstance(algorithm); - } catch (NoSuchAlgorithmException e) { - Timber.e(e, "Error while initializing MessageDigest (" + algorithm + ")"); - } - - if (digest != null) { - digest.reset(); - try { - String hash = Hex.encodeHex(digest.digest(chain[i].getEncoded())); - chainInfo.append("Fingerprint ("+ algorithm +"): ").append("\n").append(hash).append("\n"); - } catch (CertificateEncodingException e) { - Timber.e(e, "Error while encoding certificate"); - } + val hash = Hex.encodeHex(digest.digest(chain[i].encoded)) + chainInfo.append("Fingerprint ($algorithm): ").append("\n").append(hash).append("\n") + } catch (e: CertificateEncodingException) { + Timber.e(e, "Error while encoding certificate") } } } + } - // TODO: refactor with DialogFragment. - // This is difficult because we need to pass through chain[0] for onClick() - new AlertDialog.Builder(AccountSetupCheckSettings.this) + // TODO: refactor with DialogFragment. + // This is difficult because we need to pass through chain[0] for onClick() + AlertDialog.Builder(this@AccountSetupCheckSettings) .setTitle(getString(R.string.account_setup_failed_dlg_invalid_certificate_title)) - //.setMessage(getString(R.string.account_setup_failed_dlg_invalid_certificate) - .setMessage(getString(msgResId, exMessage) - + " " + chainInfo.toString() - ) + .setMessage(getString(msgResId, errorMessage) + " " + chainInfo.toString()) .setCancelable(true) - .setPositiveButton( - getString(R.string.account_setup_failed_dlg_invalid_certificate_accept), - new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, int which) { - acceptCertificate(chain[0]); - } - }) - .setNegativeButton( - getString(R.string.account_setup_failed_dlg_invalid_certificate_reject), - new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, int which) { - finish(); - } - }) - .show(); - } - }); + .setPositiveButton(R.string.account_setup_failed_dlg_invalid_certificate_accept) { _, _ -> + acceptCertificate(chain[0]) + } + .setNegativeButton(R.string.account_setup_failed_dlg_invalid_certificate_reject) { _, _ -> + finish() + } + .show() + } } /** - * Permanently accepts a certificate for the INCOMING or OUTGOING direction - * by adding it to the local key store. - * - * @param certificate + * Permanently accepts a certificate for the INCOMING or OUTGOING direction by adding it to the local key store. */ - private void acceptCertificate(X509Certificate certificate) { + private fun acceptCertificate(certificate: X509Certificate) { try { - DI.get(LocalKeyStoreManager.class).addCertificate(mAccount, mDirection.toMailServerDirection(), certificate); - } catch (CertificateException e) { - showErrorDialog( - R.string.account_setup_failed_dlg_certificate_message_fmt, - e.getMessage() == null ? "" : e.getMessage()); + localKeyStoreManager.addCertificate(account, direction.toMailServerDirection(), certificate) + } catch (e: CertificateException) { + showErrorDialog(R.string.account_setup_failed_dlg_certificate_message_fmt, e.message.orEmpty()) } - AccountSetupCheckSettings.actionCheckSettings(AccountSetupCheckSettings.this, mAccount, - mDirection); + + actionCheckSettings(this@AccountSetupCheckSettings, account, direction) } - @Override - public void onActivityResult(int reqCode, int resCode, Intent data) { + override fun onActivityResult(reqCode: Int, resCode: Int, data: Intent?) { if (reqCode == ACTIVITY_REQUEST_CODE) { - setResult(resCode); - finish(); + setResult(resCode) + finish() } else { - super.onActivityResult(reqCode, resCode, data); + super.onActivityResult(reqCode, resCode, data) } } - private void onCancel() { - mCanceled = true; - setMessage(R.string.account_setup_check_settings_canceling_msg); - setResult(RESULT_CANCELED); - finish(); + private fun onCancel() { + canceled = true + setMessage(R.string.account_setup_check_settings_canceling_msg) + + setResult(RESULT_CANCELED) + finish() } - public void onClick(View v) { - if (v.getId() == R.id.cancel) { - onCancel(); + private fun showErrorDialog(msgResId: Int, vararg args: Any) { + handler.post { + showDialogFragment(R.id.dialog_account_setup_error, getString(msgResId, *args)) } } - private void showErrorDialog(final int msgResId, final Object... args) { - mHandler.post(new Runnable() { - public void run() { - showDialogFragment(R.id.dialog_account_setup_error, getString(msgResId, args)); - } - }); - } + private fun showDialogFragment(dialogId: Int, customMessage: String) { + if (destroyed) return - private void showDialogFragment(int dialogId, String customMessage) { - if (mDestroyed) { - return; - } - mProgressBar.setIndeterminate(false); + progressBar.isIndeterminate = false - DialogFragment fragment; - if (dialogId == R.id.dialog_account_setup_error) { - fragment = ConfirmationDialogFragment.newInstance(dialogId, - getString(R.string.account_setup_failed_dlg_title), - customMessage, - getString(R.string.account_setup_failed_dlg_edit_details_action), - getString(R.string.account_setup_failed_dlg_continue_action) - ); + val fragment: DialogFragment = if (dialogId == R.id.dialog_account_setup_error) { + ConfirmationDialogFragment.newInstance( + dialogId, + getString(R.string.account_setup_failed_dlg_title), + customMessage, + getString(R.string.account_setup_failed_dlg_edit_details_action), + getString(R.string.account_setup_failed_dlg_continue_action) + ) } else { - throw new RuntimeException("Called showDialog(int) with unknown dialog id."); + throw RuntimeException("Called showDialog(int) with unknown dialog id.") } - FragmentTransaction ta = getSupportFragmentManager().beginTransaction(); - ta.add(fragment, getDialogTag(dialogId)); - ta.commitAllowingStateLoss(); - // TODO: commitAllowingStateLoss() is used to prevent https://code.google.com/p/android/issues/detail?id=23761 // but is a bad... - //fragment.show(ta, getDialogTag(dialogId)); + supportFragmentManager.commit(allowStateLoss = true) { + add(fragment, getDialogTag(dialogId)) + } } - private String getDialogTag(int dialogId) { - return String.format(Locale.US, "dialog-%d", dialogId); + private fun getDialogTag(dialogId: Int): String { + return String.format(Locale.US, "dialog-%d", dialogId) } - @Override - public void doPositiveClick(int dialogId) { + override fun doPositiveClick(dialogId: Int) { if (dialogId == R.id.dialog_account_setup_error) { - finish(); + finish() } } - @Override - public void doNegativeClick(int dialogId) { + override fun doNegativeClick(dialogId: Int) { if (dialogId == R.id.dialog_account_setup_error) { - mCanceled = false; - setResult(RESULT_OK); - finish(); + canceled = false + setResult(RESULT_OK) + finish() } } - @Override - public void dialogCancelled(int dialogId) { - // nothing to do here... - } + override fun dialogCancelled(dialogId: Int) = Unit - private String errorMessageForCertificateException(CertificateValidationException e) { - switch (e.getReason()) { - case Expired: return getString(R.string.client_certificate_expired, e.getAlias(), e.getMessage()); - case MissingCapability: return getString(R.string.auth_external_error); - case RetrievalFailure: return getString(R.string.client_certificate_retrieval_failure, e.getAlias()); - case UseMessage: return e.getMessage(); - case Unknown: - default: return ""; + private fun errorMessageForCertificateException(e: CertificateValidationException): String? { + return when (e.reason) { + CertificateValidationException.Reason.Expired -> { + getString(R.string.client_certificate_expired, e.alias, e.message) + } + CertificateValidationException.Reason.MissingCapability -> { + getString(R.string.auth_external_error) + } + CertificateValidationException.Reason.RetrievalFailure -> { + getString(R.string.client_certificate_retrieval_failure, e.alias) + } + CertificateValidationException.Reason.UseMessage -> { + e.message + } + else -> { + "" + } } } @@ -410,117 +338,125 @@ public class AccountSetupCheckSettings extends K9Activity implements OnClickList * FIXME: Don't use an AsyncTask to perform network operations. * See also discussion in https://github.com/k9mail/k-9/pull/560 */ - private class CheckAccountTask extends AsyncTask { - private final Account account; - - private CheckAccountTask(Account account) { - this.account = account; - } - - @Override - protected Void doInBackground(CheckDirection... params) { - final CheckDirection direction = params[0]; + private inner class CheckAccountTask(private val account: Account) : AsyncTask() { + override fun doInBackground(vararg params: CheckDirection) { + val direction = params[0] try { /* * This task could be interrupted at any point, but network operations can block, * so relying on InterruptedException is not enough. Instead, check after * each potentially long-running operation. */ - if (cancelled()) { - return null; + if (isCanceled()) { + return } - clearCertificateErrorNotifications(direction); + clearCertificateErrorNotifications(direction) - checkServerSettings(direction); + checkServerSettings(direction) - if (cancelled()) { - return null; + if (isCanceled()) { + return } - setResult(RESULT_OK); - finish(); - - } catch (AuthenticationFailedException afe) { - Timber.e(afe, "Error while testing settings"); - showErrorDialog( - R.string.account_setup_failed_dlg_auth_message_fmt, - afe.getMessage() == null ? "" : afe.getMessage()); - } catch (CertificateValidationException cve) { - handleCertificateValidationException(cve); - } catch (Exception e) { - Timber.e(e, "Error while testing settings"); - String message = e.getMessage() == null ? "" : e.getMessage(); - showErrorDialog(R.string.account_setup_failed_dlg_server_message_fmt, message); + setResult(RESULT_OK) + finish() + } catch (e: AuthenticationFailedException) { + Timber.e(e, "Error while testing settings") + showErrorDialog(R.string.account_setup_failed_dlg_auth_message_fmt, e.message.orEmpty()) + } catch (e: CertificateValidationException) { + handleCertificateValidationException(e) + } catch (e: Exception) { + Timber.e(e, "Error while testing settings") + showErrorDialog(R.string.account_setup_failed_dlg_server_message_fmt, e.message.orEmpty()) } - return null; } - private void clearCertificateErrorNotifications(CheckDirection direction) { - final MessagingController ctrl = MessagingController.getInstance(getApplication()); - boolean incoming = (direction == CheckDirection.INCOMING); - ctrl.clearCertificateErrorNotifications(account, incoming); + private fun clearCertificateErrorNotifications(direction: CheckDirection) { + val incoming = direction == CheckDirection.INCOMING + messagingController.clearCertificateErrorNotifications(account, incoming) } - private boolean cancelled() { - if (mDestroyed) { - return true; - } - if (mCanceled) { - finish(); - return true; + private fun isCanceled(): Boolean { + if (destroyed) return true + + if (canceled) { + finish() + return true } - return false; + + return false } - private void checkServerSettings(CheckDirection direction) throws MessagingException { - switch (direction) { - case INCOMING: { - checkIncoming(); - break; - } - case OUTGOING: { - checkOutgoing(); - break; - } + private fun checkServerSettings(direction: CheckDirection) { + when (direction) { + CheckDirection.INCOMING -> checkIncoming() + CheckDirection.OUTGOING -> checkOutgoing() } } - private void checkOutgoing() throws MessagingException { - if (!isWebDavAccount()) { - publishProgress(R.string.account_setup_check_settings_check_outgoing_msg); + private fun checkOutgoing() { + if (!isWebDavAccount) { + publishProgress(R.string.account_setup_check_settings_check_outgoing_msg) } - messagingController.checkOutgoingServerSettings(account); + messagingController.checkOutgoingServerSettings(account) } - private void checkIncoming() throws MessagingException { - if (isWebDavAccount()) { - publishProgress(R.string.account_setup_check_settings_authenticate); + private fun checkIncoming() { + if (isWebDavAccount) { + publishProgress(R.string.account_setup_check_settings_authenticate) } else { - publishProgress(R.string.account_setup_check_settings_check_incoming_msg); + publishProgress(R.string.account_setup_check_settings_check_incoming_msg) } - messagingController.checkIncomingServerSettings(account); + messagingController.checkIncomingServerSettings(account) - if (isWebDavAccount()) { - publishProgress(R.string.account_setup_check_settings_fetch); + if (isWebDavAccount) { + publishProgress(R.string.account_setup_check_settings_fetch) } - messagingController.refreshFolderListSynchronous(account); - Long inboxFolderId = account.getInboxFolderId(); + messagingController.refreshFolderListSynchronous(account) + + val inboxFolderId = account.inboxFolderId if (inboxFolderId != null) { - messagingController.synchronizeMailbox(account, inboxFolderId, false, null); + messagingController.synchronizeMailbox(account, inboxFolderId, false, null) } } - private boolean isWebDavAccount() { - return account.getIncomingServerSettings().type.equals(Protocols.WEBDAV); + private val isWebDavAccount: Boolean + get() = account.incomingServerSettings.type == Protocols.WEBDAV + + override fun onProgressUpdate(vararg values: Int?) { + setMessage(values[0]!!) + } + } + + enum class CheckDirection { + INCOMING, OUTGOING; + + fun toMailServerDirection(): MailServerDirection { + return when (this) { + INCOMING -> MailServerDirection.INCOMING + OUTGOING -> MailServerDirection.OUTGOING + } } + } + + companion object { + const val ACTIVITY_REQUEST_CODE = 1 + + private const val EXTRA_ACCOUNT = "account" + private const val EXTRA_CHECK_DIRECTION = "checkDirection" + + @JvmStatic + fun actionCheckSettings(context: Activity, account: Account, direction: CheckDirection) { + val intent = Intent(context, AccountSetupCheckSettings::class.java).apply { + putExtra(EXTRA_ACCOUNT, account.uuid) + putExtra(EXTRA_CHECK_DIRECTION, direction) + } - @Override - protected void onProgressUpdate(Integer... values) { - setMessage(values[0]); + context.startActivityForResult(intent, ACTIVITY_REQUEST_CODE) } } } -- GitLab From 60bf78d3f00d41b1413c9ce038dd30e1e397fe6a Mon Sep 17 00:00:00 2001 From: cketti Date: Sun, 22 May 2022 21:52:18 +0200 Subject: [PATCH 55/75] Ignore invalid SMTP EHLO response lines --- .../mail/transport/smtp/SmtpResponseParser.kt | 23 +++--- .../transport/smtp/SmtpResponseParserTest.kt | 77 ++++++++++--------- 2 files changed, 54 insertions(+), 46 deletions(-) diff --git a/mail/protocols/smtp/src/main/java/com/fsck/k9/mail/transport/smtp/SmtpResponseParser.kt b/mail/protocols/smtp/src/main/java/com/fsck/k9/mail/transport/smtp/SmtpResponseParser.kt index e67febebd1..a7ceefaa10 100644 --- a/mail/protocols/smtp/src/main/java/com/fsck/k9/mail/transport/smtp/SmtpResponseParser.kt +++ b/mail/protocols/smtp/src/main/java/com/fsck/k9/mail/transport/smtp/SmtpResponseParser.kt @@ -115,20 +115,25 @@ internal class SmtpResponseParser( private fun parseEhloLine(ehloLine: String, keywords: MutableMap>) { val parts = ehloLine.split(" ") - val keyword = checkAndNormalizeEhloKeyword(parts[0]) - val parameters = checkEhloParameters(parts) - if (keywords.containsKey(keyword)) { - parserError("Same EHLO keyword present in more than one response line") - } + try { + val keyword = checkAndNormalizeEhloKeyword(parts[0]) + val parameters = checkEhloParameters(parts) + + if (keywords.containsKey(keyword)) { + parserError("Same EHLO keyword present in more than one response line", logging = false) + } - keywords[keyword] = parameters + keywords[keyword] = parameters + } catch (e: SmtpResponseParserException) { + logger.log(e, "Ignoring EHLO keyword line: $ehloLine") + } } private fun checkAndNormalizeEhloKeyword(text: String): String { val keyword = text.uppercase() if (!keyword[0].isCapitalAlphaDigit() || keyword.any { !it.isCapitalAlphaDigit() && it != DASH }) { - parserError("EHLO keyword contains invalid character") + parserError("EHLO keyword contains invalid character", logging = false) } return keyword @@ -138,9 +143,9 @@ internal class SmtpResponseParser( for (i in 1..parts.lastIndex) { val parameter = parts[i] if (parameter.isEmpty()) { - parserError("EHLO parameter must not be empty") + parserError("EHLO parameter must not be empty", logging = false) } else if (parameter.any { it.code !in 33..126 }) { - parserError("EHLO parameter contains invalid character") + parserError("EHLO parameter contains invalid character", logging = false) } } diff --git a/mail/protocols/smtp/src/test/java/com/fsck/k9/mail/transport/smtp/SmtpResponseParserTest.kt b/mail/protocols/smtp/src/test/java/com/fsck/k9/mail/transport/smtp/SmtpResponseParserTest.kt index e3f349201c..9b813c0ae5 100644 --- a/mail/protocols/smtp/src/test/java/com/fsck/k9/mail/transport/smtp/SmtpResponseParserTest.kt +++ b/mail/protocols/smtp/src/test/java/com/fsck/k9/mail/transport/smtp/SmtpResponseParserTest.kt @@ -145,26 +145,36 @@ class SmtpResponseParserTest { } @Test - fun `read EHLO response with invalid keyword`() { + fun `read EHLO response with invalid keywords`() { val input = """ 250-smtp.domain.example - 250 KEY:WORD + 250-SIZE 52428800 + 250-8BITMIME + 250-PIPELINING + 250-PIPE_CONNECT + 250-AUTH=PLAIN + 250 HELP """.toPeekableInputStream() val parser = SmtpResponseParser(logger, input) - assertFailsWithMessage("EHLO keyword contains invalid character") { - parser.readHelloResponse() - } + val response = parser.readHelloResponse() - assertThat(logger.logEntries).containsExactly( - LogEntry( - throwable = null, - message = """ - SMTP response data on parser error: - 250-smtp.domain.example - 250 KEY:WORD - """.trimIndent() + assertType(response) { hello -> + assertThat(hello.keywords.keys).containsExactly( + "SIZE", + "8BITMIME", + "PIPELINING", + "HELP" ) + } + + assertThat(logger.logEntries.map { it.message }).containsExactly( + "Ignoring EHLO keyword line: PIPE_CONNECT", + "Ignoring EHLO keyword line: AUTH=PLAIN" + ) + assertThat(logger.logEntries.map { it.throwable?.message }).containsExactly( + "EHLO keyword contains invalid character", + "EHLO keyword contains invalid character" ) } @@ -176,44 +186,37 @@ class SmtpResponseParserTest { """.toPeekableInputStream() val parser = SmtpResponseParser(logger, input) - assertFailsWithMessage("EHLO parameter must not be empty") { - parser.readHelloResponse() + val response = parser.readHelloResponse() + + assertType(response) { hello -> + assertThat(hello.keywords.keys).isEmpty() } - assertThat(logger.logEntries).containsExactly( - LogEntry( - throwable = null, - message = """ - SMTP response data on parser error: - 250-smtp.domain.example - 250 KEYWORD${" "} - """.trimIndent() - ) - ) + assertThat(logger.logEntries).hasSize(1) + assertThat(logger.logEntries.first().throwable).hasMessageThat().isEqualTo("EHLO parameter must not be empty") + assertThat(logger.logEntries.first().message).isEqualTo("Ignoring EHLO keyword line: KEYWORD ") } @Test fun `read EHLO response with invalid parameter`() { val input = """ 250-smtp.domain.example + 250-8BITMIME 250 KEYWORD para${"\t"}meter """.toPeekableInputStream() val parser = SmtpResponseParser(logger, input) - assertFailsWithMessage("EHLO parameter contains invalid character") { - parser.readHelloResponse() + val response = parser.readHelloResponse() + + assertType(response) { hello -> + assertThat(hello.keywords.keys).containsExactly("8BITMIME") } - assertThat(logger.logEntries).containsExactly( - LogEntry( - throwable = null, - message = """ - SMTP response data on parser error: - 250-smtp.domain.example - 250 KEYWORD para${"\t"}meter - """.trimIndent() - ) - ) + assertThat(logger.logEntries).hasSize(1) + assertThat(logger.logEntries.first().throwable) + .hasMessageThat().isEqualTo("EHLO parameter contains invalid character") + assertThat(logger.logEntries.first().message) + .isEqualTo("Ignoring EHLO keyword line: KEYWORD para${"\t"}meter") } @Test -- GitLab From 8fa9b7fbe6dba79c31b3435a735bb7bf1aae2e97 Mon Sep 17 00:00:00 2001 From: Mihail Mitrofanov Date: Mon, 23 May 2022 16:22:20 +0200 Subject: [PATCH 56/75] Fix #5983. Missing back button Add back button in below activities: * AccountSetupComposition (item "Composition defaults") * AccountSetupOutgoing (item "Outgoing server") * AccountSetupIncoming (item "Incoming server") --- .../activity/setup/AccountSetupComposition.java | 17 +++++++++++++++++ .../k9/activity/setup/AccountSetupIncoming.java | 15 +++++++++++++++ .../k9/activity/setup/AccountSetupOutgoing.java | 15 +++++++++++++++ 3 files changed, 47 insertions(+) diff --git a/app/ui/legacy/src/main/java/com/fsck/k9/activity/setup/AccountSetupComposition.java b/app/ui/legacy/src/main/java/com/fsck/k9/activity/setup/AccountSetupComposition.java index 719952ab2a..10315606fd 100644 --- a/app/ui/legacy/src/main/java/com/fsck/k9/activity/setup/AccountSetupComposition.java +++ b/app/ui/legacy/src/main/java/com/fsck/k9/activity/setup/AccountSetupComposition.java @@ -3,12 +3,15 @@ package com.fsck.k9.activity.setup; import android.app.Activity; import android.content.Intent; import android.os.Bundle; +import android.view.MenuItem; import android.view.View; import android.widget.CompoundButton; import android.widget.CheckBox; import android.widget.EditText; import android.widget.LinearLayout; import android.widget.RadioButton; + +import androidx.annotation.NonNull; import com.fsck.k9.Account; import com.fsck.k9.Preferences; import com.fsck.k9.ui.R; @@ -48,6 +51,10 @@ public class AccountSetupComposition extends K9Activity { setLayout(R.layout.account_setup_composition); setTitle(R.string.account_settings_composition_title); + if (getSupportActionBar() != null) { + getSupportActionBar().setDisplayHomeAsUpEnabled(true); + } + /* * If we're being reloaded we override the original account with the one * we saved @@ -101,6 +108,16 @@ public class AccountSetupComposition extends K9Activity { } } + @Override + public boolean onOptionsItemSelected(@NonNull MenuItem item) { + if (item.getItemId() == android.R.id.home) { + onBackPressed(); + return true; + } + + return super.onOptionsItemSelected(item); + } + private void saveSettings() { mAccount.setEmail(mAccountEmail.getText().toString()); mAccount.setAlwaysBcc(mAccountAlwaysBcc.getText().toString()); diff --git a/app/ui/legacy/src/main/java/com/fsck/k9/activity/setup/AccountSetupIncoming.java b/app/ui/legacy/src/main/java/com/fsck/k9/activity/setup/AccountSetupIncoming.java index 16decb409d..8cea8f712e 100644 --- a/app/ui/legacy/src/main/java/com/fsck/k9/activity/setup/AccountSetupIncoming.java +++ b/app/ui/legacy/src/main/java/com/fsck/k9/activity/setup/AccountSetupIncoming.java @@ -9,6 +9,7 @@ import android.os.Bundle; import android.text.Editable; import android.text.TextWatcher; import android.text.method.DigitsKeyListener; +import android.view.MenuItem; import android.view.View; import android.view.View.OnClickListener; import android.view.ViewGroup; @@ -117,6 +118,10 @@ public class AccountSetupIncoming extends K9Activity implements OnClickListener setLayout(R.layout.account_setup_incoming); setTitle(R.string.account_setup_incoming_title); + if (getSupportActionBar() != null) { + getSupportActionBar().setDisplayHomeAsUpEnabled(true); + } + mUsernameView = findViewById(R.id.account_username); mPasswordView = findViewById(R.id.account_password); mClientCertificateSpinner = findViewById(R.id.account_client_certificate_spinner); @@ -563,6 +568,16 @@ public class AccountSetupIncoming extends K9Activity implements OnClickListener } } + @Override + public boolean onOptionsItemSelected(@NonNull MenuItem item) { + if (item.getItemId() == android.R.id.home) { + onBackPressed(); + return true; + } + + return super.onOptionsItemSelected(item); + } + protected void onNext() { try { ConnectionSecurity connectionSecurity = getSelectedSecurity(); diff --git a/app/ui/legacy/src/main/java/com/fsck/k9/activity/setup/AccountSetupOutgoing.java b/app/ui/legacy/src/main/java/com/fsck/k9/activity/setup/AccountSetupOutgoing.java index 8e64281ae7..1173e71f60 100644 --- a/app/ui/legacy/src/main/java/com/fsck/k9/activity/setup/AccountSetupOutgoing.java +++ b/app/ui/legacy/src/main/java/com/fsck/k9/activity/setup/AccountSetupOutgoing.java @@ -10,6 +10,7 @@ import android.os.Bundle; import android.text.Editable; import android.text.TextWatcher; import android.text.method.DigitsKeyListener; +import android.view.MenuItem; import android.view.View; import android.view.View.OnClickListener; import android.view.ViewGroup; @@ -99,6 +100,10 @@ public class AccountSetupOutgoing extends K9Activity implements OnClickListener, setLayout(R.layout.account_setup_outgoing); setTitle(R.string.account_setup_outgoing_title); + if (getSupportActionBar() != null) { + getSupportActionBar().setDisplayHomeAsUpEnabled(true); + } + String accountUuid = getIntent().getStringExtra(EXTRA_ACCOUNT); mAccount = Preferences.getPreferences(this).getAccount(accountUuid); @@ -348,6 +353,16 @@ public class AccountSetupOutgoing extends K9Activity implements OnClickListener, validateFields(); } + @Override + public boolean onOptionsItemSelected(@NonNull MenuItem item) { + if (item.getItemId() == android.R.id.home) { + onBackPressed(); + return true; + } + + return super.onOptionsItemSelected(item); + } + /** * Shows/hides password field */ -- GitLab From 14a7f1b0d3a0409234f8075956f836248c7ef7d5 Mon Sep 17 00:00:00 2001 From: cketti Date: Sat, 21 May 2022 17:27:25 +0200 Subject: [PATCH 57/75] Add support for OAuth 2.0 (Gmail) --- .../providersxml/ProvidersXmlDiscovery.kt | 18 +- .../providersxml/ProvidersXmlDiscoveryTest.kt | 4 +- app/core/src/main/java/com/fsck/k9/Account.kt | 4 + .../fsck/k9/AccountPreferenceSerializer.kt | 3 + .../src/main/java/com/fsck/k9/Preferences.kt | 2 +- .../com/fsck/k9/preferences/AccountManager.kt | 1 + app/k9mail/build.gradle | 16 +- .../src/main/java/com/fsck/k9/Dependencies.kt | 3 + .../com/fsck/k9/auth/K9OAuthCredentials.kt | 9 + .../fsck/k9/backends/ImapBackendFactory.kt | 20 +- .../java/com/fsck/k9/backends/KoinModule.kt | 3 +- .../k9/backends/RealOAuth2TokenProvider.kt | 65 +++++ app/ui/legacy/build.gradle | 1 + .../java/com/fsck/k9/activity/KoinModule.kt | 5 +- .../k9/activity/setup/AccountSetupBasics.kt | 75 ++++- .../setup/AccountSetupCheckSettings.kt | 54 +++- .../activity/setup/AccountSetupIncoming.java | 35 +-- .../activity/setup/AccountSetupOutgoing.java | 41 +-- .../k9/activity/setup/AuthTypeAdapter.java | 10 +- .../k9/activity/setup/AuthTypeHolder.java | 3 +- .../fsck/k9/activity/setup/AuthViewModel.kt | 269 ++++++++++++++++++ .../k9/activity/setup/OAuthConfiguration.kt | 8 + .../k9/activity/setup/OAuthCredentials.kt | 5 + .../main/res/layout/account_setup_basics.xml | 1 + app/ui/legacy/src/main/res/values/strings.xml | 5 + app/ui/legacy/src/test/AndroidManifest.xml | 21 ++ debug.keystore | Bin 0 -> 2651 bytes .../k9/mail/oauth/OAuth2TokenProvider.java | 11 +- .../mail/store/imap/RealImapConnection.java | 6 +- .../mail/store/imap/RealImapConnectionTest.kt | 10 +- .../k9/mail/transport/smtp/SmtpTransport.kt | 6 +- .../mail/transport/smtp/SmtpTransportTest.kt | 32 +-- 32 files changed, 651 insertions(+), 95 deletions(-) create mode 100644 app/k9mail/src/main/java/com/fsck/k9/auth/K9OAuthCredentials.kt create mode 100644 app/k9mail/src/main/java/com/fsck/k9/backends/RealOAuth2TokenProvider.kt create mode 100644 app/ui/legacy/src/main/java/com/fsck/k9/activity/setup/AuthViewModel.kt create mode 100644 app/ui/legacy/src/main/java/com/fsck/k9/activity/setup/OAuthConfiguration.kt create mode 100644 app/ui/legacy/src/main/java/com/fsck/k9/activity/setup/OAuthCredentials.kt create mode 100644 app/ui/legacy/src/test/AndroidManifest.xml create mode 100644 debug.keystore diff --git a/app/autodiscovery/providersxml/src/main/java/com/fsck/k9/autodiscovery/providersxml/ProvidersXmlDiscovery.kt b/app/autodiscovery/providersxml/src/main/java/com/fsck/k9/autodiscovery/providersxml/ProvidersXmlDiscovery.kt index ef7651fe10..0992656beb 100644 --- a/app/autodiscovery/providersxml/src/main/java/com/fsck/k9/autodiscovery/providersxml/ProvidersXmlDiscovery.kt +++ b/app/autodiscovery/providersxml/src/main/java/com/fsck/k9/autodiscovery/providersxml/ProvidersXmlDiscovery.kt @@ -104,7 +104,14 @@ class ProvidersXmlDiscovery( uri.port } - return DiscoveredServerSettings(Protocols.IMAP, host, port, security, AuthType.PLAIN, username) + // TODO: Remove this hack + val authType = if (host == "imap.gmail.com" || host == "imap.googlemail.com") { + AuthType.XOAUTH2 + } else { + AuthType.PLAIN + } + + return DiscoveredServerSettings(Protocols.IMAP, host, port, security, authType, username) } private fun Provider.toOutgoingServerSettings(email: String): DiscoveredServerSettings? { @@ -127,7 +134,14 @@ class ProvidersXmlDiscovery( uri.port } - return DiscoveredServerSettings(Protocols.SMTP, host, port, security, AuthType.PLAIN, username) + // TODO: Remove this hack + val authType = if (host == "smtp.gmail.com" || host == "smtp.googlemail.com") { + AuthType.XOAUTH2 + } else { + AuthType.PLAIN + } + + return DiscoveredServerSettings(Protocols.SMTP, host, port, security, authType, username) } private fun String.fillInUsernameTemplate(email: String, user: String, domain: String): String { diff --git a/app/autodiscovery/providersxml/src/test/java/com/fsck/k9/autodiscovery/providersxml/ProvidersXmlDiscoveryTest.kt b/app/autodiscovery/providersxml/src/test/java/com/fsck/k9/autodiscovery/providersxml/ProvidersXmlDiscoveryTest.kt index b1bf51c501..5f36f32239 100644 --- a/app/autodiscovery/providersxml/src/test/java/com/fsck/k9/autodiscovery/providersxml/ProvidersXmlDiscoveryTest.kt +++ b/app/autodiscovery/providersxml/src/test/java/com/fsck/k9/autodiscovery/providersxml/ProvidersXmlDiscoveryTest.kt @@ -20,13 +20,13 @@ class ProvidersXmlDiscoveryTest : RobolectricTest() { with(connectionSettings!!.incoming.first()) { assertThat(host).isEqualTo("imap.gmail.com") assertThat(security).isEqualTo(ConnectionSecurity.SSL_TLS_REQUIRED) - assertThat(authType).isEqualTo(AuthType.PLAIN) + assertThat(authType).isEqualTo(AuthType.XOAUTH2) assertThat(username).isEqualTo("user@gmail.com") } with(connectionSettings.outgoing.first()) { assertThat(host).isEqualTo("smtp.gmail.com") assertThat(security).isEqualTo(ConnectionSecurity.SSL_TLS_REQUIRED) - assertThat(authType).isEqualTo(AuthType.PLAIN) + assertThat(authType).isEqualTo(AuthType.XOAUTH2) assertThat(username).isEqualTo("user@gmail.com") } } diff --git a/app/core/src/main/java/com/fsck/k9/Account.kt b/app/core/src/main/java/com/fsck/k9/Account.kt index 2195e1fceb..454007a79d 100644 --- a/app/core/src/main/java/com/fsck/k9/Account.kt +++ b/app/core/src/main/java/com/fsck/k9/Account.kt @@ -34,6 +34,10 @@ class Account(override val uuid: String) : BaseAccount { internalOutgoingServerSettings = value } + @get:Synchronized + @set:Synchronized + var oAuthState: String? = null + /** * Storage provider ID, used to locate and manage the underlying DB/file storage. */ diff --git a/app/core/src/main/java/com/fsck/k9/AccountPreferenceSerializer.kt b/app/core/src/main/java/com/fsck/k9/AccountPreferenceSerializer.kt index 3bb834d7b1..00bafbdb70 100644 --- a/app/core/src/main/java/com/fsck/k9/AccountPreferenceSerializer.kt +++ b/app/core/src/main/java/com/fsck/k9/AccountPreferenceSerializer.kt @@ -36,6 +36,7 @@ class AccountPreferenceSerializer( outgoingServerSettings = serverSettingsSerializer.deserialize( storage.getString("$accountUuid.$OUTGOING_SERVER_SETTINGS_KEY", "") ) + oAuthState = storage.getString("$accountUuid.oAuthState", null) localStorageProviderId = storage.getString("$accountUuid.localStorageProvider", storageManager.defaultProviderId) name = storage.getString("$accountUuid.description", null) alwaysBcc = storage.getString("$accountUuid.alwaysBcc", alwaysBcc) @@ -240,6 +241,7 @@ class AccountPreferenceSerializer( with(account) { editor.putString("$accountUuid.$INCOMING_SERVER_SETTINGS_KEY", serverSettingsSerializer.serialize(incomingServerSettings)) editor.putString("$accountUuid.$OUTGOING_SERVER_SETTINGS_KEY", serverSettingsSerializer.serialize(outgoingServerSettings)) + editor.putString("$accountUuid.oAuthState", oAuthState) editor.putString("$accountUuid.localStorageProvider", localStorageProviderId) editor.putString("$accountUuid.description", name) editor.putString("$accountUuid.alwaysBcc", alwaysBcc) @@ -359,6 +361,7 @@ class AccountPreferenceSerializer( editor.remove("$accountUuid.$INCOMING_SERVER_SETTINGS_KEY") editor.remove("$accountUuid.$OUTGOING_SERVER_SETTINGS_KEY") + editor.remove("$accountUuid.oAuthState") editor.remove("$accountUuid.description") editor.remove("$accountUuid.name") editor.remove("$accountUuid.email") diff --git a/app/core/src/main/java/com/fsck/k9/Preferences.kt b/app/core/src/main/java/com/fsck/k9/Preferences.kt index f7eaab83d9..eca0f09fb1 100644 --- a/app/core/src/main/java/com/fsck/k9/Preferences.kt +++ b/app/core/src/main/java/com/fsck/k9/Preferences.kt @@ -201,7 +201,7 @@ class Preferences internal constructor( val defaultAccount: Account? get() = accounts.firstOrNull() - fun saveAccount(account: Account) { + override fun saveAccount(account: Account) { ensureAssignedAccountNumber(account) processChangedValues(account) diff --git a/app/core/src/main/java/com/fsck/k9/preferences/AccountManager.kt b/app/core/src/main/java/com/fsck/k9/preferences/AccountManager.kt index 5ae56c7e1d..4037932c77 100644 --- a/app/core/src/main/java/com/fsck/k9/preferences/AccountManager.kt +++ b/app/core/src/main/java/com/fsck/k9/preferences/AccountManager.kt @@ -13,4 +13,5 @@ interface AccountManager { fun moveAccount(account: Account, newPosition: Int) fun addOnAccountsChangeListener(accountsChangeListener: AccountsChangeListener) fun removeOnAccountsChangeListener(accountsChangeListener: AccountsChangeListener) + fun saveAccount(account: Account) } diff --git a/app/k9mail/build.gradle b/app/k9mail/build.gradle index aae39b45b1..80c8b92a51 100644 --- a/app/k9mail/build.gradle +++ b/app/k9mail/build.gradle @@ -65,6 +65,12 @@ android { signingConfigs { release + debug { + keyAlias = "androiddebugkey" + keyPassword = "android" + storeFile = file("$rootProject.projectDir/debug.keystore") + storePassword = "android" + } } buildTypes { @@ -76,16 +82,22 @@ android { minifyEnabled true proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' - buildConfigField "boolean", "DEVELOPER_MODE", "false" + buildConfigField "String", "OAUTH_GMAIL_CLIENT_ID", "\"262622259280-hhmh92rhklkg2k1tjil69epo0o9a12jm.apps.googleusercontent.com\"" + + manifestPlaceholders = ['appAuthRedirectScheme': 'com.fsck.k9'] } debug { applicationIdSuffix ".debug" testCoverageEnabled rootProject.testCoverage + signingConfig signingConfigs.debug + minifyEnabled false - buildConfigField "boolean", "DEVELOPER_MODE", "true" + buildConfigField "String", "OAUTH_GMAIL_CLIENT_ID", "\"262622259280-5qb3vtj68d5dtudmaif4g9vd3cpar8r3.apps.googleusercontent.com\"" + + manifestPlaceholders = ['appAuthRedirectScheme': 'com.fsck.k9.debug'] } } diff --git a/app/k9mail/src/main/java/com/fsck/k9/Dependencies.kt b/app/k9mail/src/main/java/com/fsck/k9/Dependencies.kt index ff39743fcd..f319af9252 100644 --- a/app/k9mail/src/main/java/com/fsck/k9/Dependencies.kt +++ b/app/k9mail/src/main/java/com/fsck/k9/Dependencies.kt @@ -1,5 +1,7 @@ package com.fsck.k9 +import com.fsck.k9.activity.setup.OAuthCredentials +import com.fsck.k9.auth.K9OAuthCredentials import com.fsck.k9.backends.backendsModule import com.fsck.k9.controller.ControllerExtension import com.fsck.k9.crypto.EncryptionExtractor @@ -29,6 +31,7 @@ private val mainAppModule = module { single(named("controllerExtensions")) { emptyList() } single { OpenPgpEncryptionExtractor.newInstance() } single { K9StoragePersister(get()) } + factory { K9OAuthCredentials() } } val appModules = listOf( diff --git a/app/k9mail/src/main/java/com/fsck/k9/auth/K9OAuthCredentials.kt b/app/k9mail/src/main/java/com/fsck/k9/auth/K9OAuthCredentials.kt new file mode 100644 index 0000000000..281cfcc4ee --- /dev/null +++ b/app/k9mail/src/main/java/com/fsck/k9/auth/K9OAuthCredentials.kt @@ -0,0 +1,9 @@ +package com.fsck.k9.auth + +import com.fsck.k9.BuildConfig +import com.fsck.k9.activity.setup.OAuthCredentials + +class K9OAuthCredentials : OAuthCredentials { + override val gmailClientId: String + get() = BuildConfig.OAUTH_GMAIL_CLIENT_ID +} diff --git a/app/k9mail/src/main/java/com/fsck/k9/backends/ImapBackendFactory.kt b/app/k9mail/src/main/java/com/fsck/k9/backends/ImapBackendFactory.kt index 989f428655..2b14b1b0ff 100644 --- a/app/k9mail/src/main/java/com/fsck/k9/backends/ImapBackendFactory.kt +++ b/app/k9mail/src/main/java/com/fsck/k9/backends/ImapBackendFactory.kt @@ -1,11 +1,12 @@ package com.fsck.k9.backends +import android.content.Context import com.fsck.k9.Account import com.fsck.k9.backend.BackendFactory import com.fsck.k9.backend.api.Backend import com.fsck.k9.backend.imap.ImapBackend import com.fsck.k9.backend.imap.ImapPushConfigProvider -import com.fsck.k9.mail.oauth.OAuth2TokenProvider +import com.fsck.k9.mail.AuthType import com.fsck.k9.mail.power.PowerManager import com.fsck.k9.mail.ssl.TrustedSocketFactory import com.fsck.k9.mail.store.imap.IdleRefreshManager @@ -23,7 +24,8 @@ class ImapBackendFactory( private val powerManager: PowerManager, private val idleRefreshManager: IdleRefreshManager, private val backendStorageFactory: K9BackendStorageFactory, - private val trustedSocketFactory: TrustedSocketFactory + private val trustedSocketFactory: TrustedSocketFactory, + private val context: Context ) : BackendFactory { override fun createBackend(account: Account): Backend { val accountName = account.displayName @@ -44,7 +46,12 @@ class ImapBackendFactory( } private fun createImapStore(account: Account): ImapStore { - val oAuth2TokenProvider: OAuth2TokenProvider? = null + val oAuth2TokenProvider = if (account.incomingServerSettings.authenticationType == AuthType.XOAUTH2) { + RealOAuth2TokenProvider(context, accountManager, account) + } else { + null + } + val config = createImapStoreConfig(account) return ImapStore.create( account.incomingServerSettings, @@ -67,7 +74,12 @@ class ImapBackendFactory( private fun createSmtpTransport(account: Account): SmtpTransport { val serverSettings = account.outgoingServerSettings - val oauth2TokenProvider: OAuth2TokenProvider? = null + val oauth2TokenProvider = if (serverSettings.authenticationType == AuthType.XOAUTH2) { + RealOAuth2TokenProvider(context, accountManager, account) + } else { + null + } + return SmtpTransport(serverSettings, trustedSocketFactory, oauth2TokenProvider) } diff --git a/app/k9mail/src/main/java/com/fsck/k9/backends/KoinModule.kt b/app/k9mail/src/main/java/com/fsck/k9/backends/KoinModule.kt index 5585a2bdcc..b23177632c 100644 --- a/app/k9mail/src/main/java/com/fsck/k9/backends/KoinModule.kt +++ b/app/k9mail/src/main/java/com/fsck/k9/backends/KoinModule.kt @@ -26,7 +26,8 @@ val backendsModule = module { powerManager = get(), idleRefreshManager = get(), backendStorageFactory = get(), - trustedSocketFactory = get() + trustedSocketFactory = get(), + context = get() ) } single { AndroidAlarmManager(context = get(), alarmManager = get()) } diff --git a/app/k9mail/src/main/java/com/fsck/k9/backends/RealOAuth2TokenProvider.kt b/app/k9mail/src/main/java/com/fsck/k9/backends/RealOAuth2TokenProvider.kt new file mode 100644 index 0000000000..a282897206 --- /dev/null +++ b/app/k9mail/src/main/java/com/fsck/k9/backends/RealOAuth2TokenProvider.kt @@ -0,0 +1,65 @@ +package com.fsck.k9.backends + +import android.content.Context +import com.fsck.k9.Account +import com.fsck.k9.mail.AuthenticationFailedException +import com.fsck.k9.mail.oauth.OAuth2TokenProvider +import com.fsck.k9.preferences.AccountManager +import java.util.concurrent.CountDownLatch +import java.util.concurrent.TimeUnit +import net.openid.appauth.AuthState +import net.openid.appauth.AuthorizationException +import net.openid.appauth.AuthorizationService + +class RealOAuth2TokenProvider( + context: Context, + private val accountManager: AccountManager, + private val account: Account +) : OAuth2TokenProvider { + private val authService = AuthorizationService(context) + private var requestFreshToken = false + + override fun getToken(timeoutMillis: Long): String { + val latch = CountDownLatch(1) + var token: String? = null + var exception: AuthorizationException? = null + + val authState = account.oAuthState?.let { AuthState.jsonDeserialize(it) } + ?: throw AuthenticationFailedException("Login required") + + if (requestFreshToken) { + authState.needsTokenRefresh = true + } + + val oldAccessToken = authState.accessToken + + authState.performActionWithFreshTokens(authService) { accessToken: String?, _, authException: AuthorizationException? -> + token = accessToken + exception = authException + + latch.countDown() + } + + latch.await(timeoutMillis, TimeUnit.MILLISECONDS) + + if (exception != null || token != oldAccessToken) { + requestFreshToken = false + account.oAuthState = authState.jsonSerializeString() + accountManager.saveAccount(account) + } + + exception?.let { authException -> + throw AuthenticationFailedException( + message = "Failed to fetch an access token", + throwable = authException, + messageFromServer = authException.error + ) + } + + return token ?: throw AuthenticationFailedException("Failed to fetch an access token") + } + + override fun invalidateToken() { + requestFreshToken = true + } +} diff --git a/app/ui/legacy/build.gradle b/app/ui/legacy/build.gradle index d7d82b89dc..ee479bd31b 100644 --- a/app/ui/legacy/build.gradle +++ b/app/ui/legacy/build.gradle @@ -40,6 +40,7 @@ dependencies { implementation "com.mikepenz:fastadapter-extensions-drag:${versions.fastAdapter}" implementation "com.mikepenz:fastadapter-extensions-utils:${versions.fastAdapter}" implementation 'de.hdodenhof:circleimageview:3.1.0' + api 'net.openid:appauth:0.11.1' implementation "commons-io:commons-io:${versions.commonsIo}" implementation "androidx.core:core-ktx:${versions.androidxCore}" diff --git a/app/ui/legacy/src/main/java/com/fsck/k9/activity/KoinModule.kt b/app/ui/legacy/src/main/java/com/fsck/k9/activity/KoinModule.kt index 93734a0061..f25afc496c 100644 --- a/app/ui/legacy/src/main/java/com/fsck/k9/activity/KoinModule.kt +++ b/app/ui/legacy/src/main/java/com/fsck/k9/activity/KoinModule.kt @@ -1,7 +1,10 @@ package com.fsck.k9.activity +import com.fsck.k9.activity.setup.AuthViewModel +import org.koin.androidx.viewmodel.dsl.viewModel import org.koin.dsl.module val activityModule = module { - single { MessageLoaderHelperFactory(get(), get()) } + single { MessageLoaderHelperFactory(messageViewInfoExtractorFactory = get(), htmlSettingsProvider = get()) } + viewModel { AuthViewModel(application = get(), accountManager = get(), oauthCredentials = get()) } } diff --git a/app/ui/legacy/src/main/java/com/fsck/k9/activity/setup/AccountSetupBasics.kt b/app/ui/legacy/src/main/java/com/fsck/k9/activity/setup/AccountSetupBasics.kt index 41d2c0df8d..d3c6c4caef 100644 --- a/app/ui/legacy/src/main/java/com/fsck/k9/activity/setup/AccountSetupBasics.kt +++ b/app/ui/legacy/src/main/java/com/fsck/k9/activity/setup/AccountSetupBasics.kt @@ -4,6 +4,7 @@ import android.content.Context import android.content.Intent import android.os.Bundle import android.text.Editable +import android.view.View import android.view.ViewGroup import android.widget.Button import android.widget.CheckBox @@ -25,6 +26,8 @@ import com.fsck.k9.mailstore.SpecialLocalFoldersCreator import com.fsck.k9.ui.ConnectionSettings import com.fsck.k9.ui.R import com.fsck.k9.ui.base.K9Activity +import com.fsck.k9.ui.getEnum +import com.fsck.k9.ui.putEnum import com.fsck.k9.ui.settings.ExtraAccountDiscovery import com.fsck.k9.view.ClientCertificateSpinner import com.google.android.material.textfield.TextInputEditText @@ -46,12 +49,15 @@ class AccountSetupBasics : K9Activity() { private lateinit var emailView: TextInputEditText private lateinit var passwordView: TextInputEditText + private lateinit var passwordLayout: View private lateinit var clientCertificateCheckBox: CheckBox private lateinit var clientCertificateSpinner: ClientCertificateSpinner + private lateinit var advancedOptionsContainer: View private lateinit var nextButton: Button private lateinit var manualSetupButton: Button private lateinit var allowClientCertificateView: ViewGroup + private var uiState = UiState.EMAIL_ADDRESS_ONLY private var account: Account? = null private var checkedIncoming = false @@ -62,14 +68,15 @@ class AccountSetupBasics : K9Activity() { emailView = findViewById(R.id.account_email) passwordView = findViewById(R.id.account_password) + passwordLayout = findViewById(R.id.account_password_layout) clientCertificateCheckBox = findViewById(R.id.account_client_certificate) clientCertificateSpinner = findViewById(R.id.account_client_certificate_spinner) allowClientCertificateView = findViewById(R.id.account_allow_client_certificate) + advancedOptionsContainer = findViewById(R.id.foldable_advanced_options) nextButton = findViewById(R.id.next) manualSetupButton = findViewById(R.id.manual_setup) manualSetupButton.setOnClickListener { onManualSetup() } - nextButton.setOnClickListener { onNext() } } override fun onPostCreate(savedInstanceState: Bundle?) { @@ -82,12 +89,15 @@ class AccountSetupBasics : K9Activity() { */ initializeViewListeners() validateFields() + + updateUi() } private fun initializeViewListeners() { val textWatcher = object : SimpleTextWatcher() { override fun afterTextChanged(s: Editable) { - validateFields() + val checkPassword = uiState == UiState.PASSWORD_FLOW + validateFields(checkPassword) } } @@ -109,9 +119,25 @@ class AccountSetupBasics : K9Activity() { } } + private fun updateUi() { + when (uiState) { + UiState.EMAIL_ADDRESS_ONLY -> { + passwordLayout.isVisible = false + advancedOptionsContainer.isVisible = false + nextButton.setOnClickListener { attemptAutoSetupUsingOnlyEmailAddress() } + } + UiState.PASSWORD_FLOW -> { + passwordLayout.isVisible = true + advancedOptionsContainer.isVisible = true + nextButton.setOnClickListener { attemptAutoSetup() } + } + } + } + override fun onSaveInstanceState(outState: Bundle) { super.onSaveInstanceState(outState) + outState.putEnum(STATE_KEY_UI_STATE, uiState) outState.putString(EXTRA_ACCOUNT, account?.uuid) outState.putBoolean(STATE_KEY_CHECKED_INCOMING, checkedIncoming) } @@ -119,6 +145,8 @@ class AccountSetupBasics : K9Activity() { override fun onRestoreInstanceState(savedInstanceState: Bundle) { super.onRestoreInstanceState(savedInstanceState) + uiState = savedInstanceState.getEnum(STATE_KEY_UI_STATE, UiState.EMAIL_ADDRESS_ONLY) + val accountUuid = savedInstanceState.getString(EXTRA_ACCOUNT) if (accountUuid != null) { account = preferences.getAccount(accountUuid) @@ -132,9 +160,10 @@ class AccountSetupBasics : K9Activity() { allowClientCertificateView.isVisible = usingCertificates } - private fun validateFields() { + private fun validateFields(checkPassword: Boolean = true) { val email = emailView.text?.toString().orEmpty() - val valid = requiredFieldValid(emailView) && emailValidator.isValidAddressOnly(email) && isPasswordFieldValid() + val valid = requiredFieldValid(emailView) && emailValidator.isValidAddressOnly(email) && + (!checkPassword || isPasswordFieldValid()) nextButton.isEnabled = valid nextButton.isFocusable = valid @@ -149,7 +178,37 @@ class AccountSetupBasics : K9Activity() { clientCertificateChecked && clientCertificateAlias != null } - private fun onNext() { + private fun attemptAutoSetupUsingOnlyEmailAddress() { + val email = emailView.text?.toString() ?: error("Email missing") + + val extraConnectionSettings = ExtraAccountDiscovery.discover(email) + if (extraConnectionSettings != null) { + finishAutoSetup(extraConnectionSettings) + return + } + + val connectionSettings = providersXmlDiscoveryDiscover(email) + + if (connectionSettings != null && + connectionSettings.incoming.authenticationType == AuthType.XOAUTH2 && + connectionSettings.outgoing.authenticationType == AuthType.XOAUTH2 + ) { + finishAutoSetup(connectionSettings) + } else { + startPasswordFlow() + } + } + + private fun startPasswordFlow() { + uiState = UiState.PASSWORD_FLOW + + updateUi() + validateFields() + + passwordView.requestFocus() + } + + private fun attemptAutoSetup() { if (clientCertificateCheckBox.isChecked) { // Auto-setup doesn't support client certificates. onManualSetup() @@ -272,8 +331,14 @@ class AccountSetupBasics : K9Activity() { } } + private enum class UiState { + EMAIL_ADDRESS_ONLY, + PASSWORD_FLOW + } + companion object { private const val EXTRA_ACCOUNT = "com.fsck.k9.AccountSetupBasics.account" + private const val STATE_KEY_UI_STATE = "com.fsck.k9.AccountSetupBasics.uiState" private const val STATE_KEY_CHECKED_INCOMING = "com.fsck.k9.AccountSetupBasics.checkedIncoming" @JvmStatic diff --git a/app/ui/legacy/src/main/java/com/fsck/k9/activity/setup/AccountSetupCheckSettings.kt b/app/ui/legacy/src/main/java/com/fsck/k9/activity/setup/AccountSetupCheckSettings.kt index dd2b71c4de..c6e88d1796 100644 --- a/app/ui/legacy/src/main/java/com/fsck/k9/activity/setup/AccountSetupCheckSettings.kt +++ b/app/ui/legacy/src/main/java/com/fsck/k9/activity/setup/AccountSetupCheckSettings.kt @@ -18,6 +18,7 @@ import com.fsck.k9.Preferences import com.fsck.k9.controller.MessagingController import com.fsck.k9.fragment.ConfirmationDialogFragment import com.fsck.k9.fragment.ConfirmationDialogFragment.ConfirmationDialogFragmentListener +import com.fsck.k9.mail.AuthType import com.fsck.k9.mail.AuthenticationFailedException import com.fsck.k9.mail.CertificateValidationException import com.fsck.k9.mail.MailServerDirection @@ -25,13 +26,16 @@ import com.fsck.k9.mail.filter.Hex import com.fsck.k9.preferences.Protocols import com.fsck.k9.ui.R import com.fsck.k9.ui.base.K9Activity +import com.fsck.k9.ui.observe import java.security.MessageDigest import java.security.NoSuchAlgorithmException import java.security.cert.CertificateEncodingException import java.security.cert.CertificateException import java.security.cert.X509Certificate import java.util.Locale +import java.util.concurrent.Executors import org.koin.android.ext.android.inject +import org.koin.androidx.viewmodel.ext.android.viewModel import timber.log.Timber /** @@ -41,6 +45,8 @@ import timber.log.Timber * while its thread is running. */ class AccountSetupCheckSettings : K9Activity(), ConfirmationDialogFragmentListener { + private val authViewModel: AuthViewModel by viewModel() + private val messagingController: MessagingController by inject() private val preferences: Preferences by inject() private val localKeyStoreManager: LocalKeyStoreManager by inject() @@ -63,6 +69,33 @@ class AccountSetupCheckSettings : K9Activity(), ConfirmationDialogFragmentListen super.onCreate(savedInstanceState) setLayout(R.layout.account_setup_check_settings) + authViewModel.init(activityResultRegistry, lifecycle) + + authViewModel.uiState.observe(this) { state -> + when (state) { + AuthFlowState.Idle -> { + return@observe + } + AuthFlowState.Success -> { + startCheckServerSettings() + } + AuthFlowState.Canceled -> { + showErrorDialog(R.string.account_setup_failed_dlg_oauth_flow_canceled) + } + is AuthFlowState.Failed -> { + showErrorDialog(R.string.account_setup_failed_dlg_oauth_flow_failed, state) + } + AuthFlowState.NotSupported -> { + showErrorDialog(R.string.account_setup_failed_dlg_oauth_not_supported) + } + AuthFlowState.BrowserNotFound -> { + showErrorDialog(R.string.account_setup_failed_dlg_browser_not_found) + } + } + + authViewModel.authResultConsumed() + } + messageView = findViewById(R.id.message) progressBar = findViewById(R.id.progress) findViewById(R.id.cancel).setOnClickListener { onCancel() } @@ -75,7 +108,26 @@ class AccountSetupCheckSettings : K9Activity(), ConfirmationDialogFragmentListen direction = intent.getSerializableExtra(EXTRA_CHECK_DIRECTION) as CheckDirection? ?: error("Missing CheckDirection") - CheckAccountTask(account).execute(direction) + if (savedInstanceState == null) { + if (needsAuthorization()) { + setMessage(R.string.account_setup_check_settings_authenticate) + authViewModel.login(account) + } else { + startCheckServerSettings() + } + } + } + + private fun needsAuthorization(): Boolean { + return ( + account.incomingServerSettings.authenticationType == AuthType.XOAUTH2 || + account.outgoingServerSettings.authenticationType == AuthType.XOAUTH2 + ) && + !authViewModel.isAuthorized(account) + } + + private fun startCheckServerSettings() { + CheckAccountTask(account).executeOnExecutor(Executors.newSingleThreadExecutor(), direction) } private fun handleCertificateValidationException(exception: CertificateValidationException) { diff --git a/app/ui/legacy/src/main/java/com/fsck/k9/activity/setup/AccountSetupIncoming.java b/app/ui/legacy/src/main/java/com/fsck/k9/activity/setup/AccountSetupIncoming.java index 16decb409d..b03cc1141e 100644 --- a/app/ui/legacy/src/main/java/com/fsck/k9/activity/setup/AccountSetupIncoming.java +++ b/app/ui/legacy/src/main/java/com/fsck/k9/activity/setup/AccountSetupIncoming.java @@ -151,9 +151,6 @@ public class AccountSetupIncoming extends K9Activity implements OnClickListener } }); - mAuthTypeAdapter = AuthTypeAdapter.get(this); - mAuthTypeView.setAdapter(mAuthTypeAdapter); - /* * Only allow digits in the port field. */ @@ -172,6 +169,10 @@ public class AccountSetupIncoming extends K9Activity implements OnClickListener mAccount = Preferences.getPreferences(this).getAccount(accountUuid); } + boolean oAuthSupported = mAccount.getIncomingServerSettings().type.equals(Protocols.IMAP); + mAuthTypeAdapter = AuthTypeAdapter.get(this, oAuthSupported); + mAuthTypeView.setAdapter(mAuthTypeAdapter); + boolean editSettings = Intent.ACTION_EDIT.equals(getIntent().getAction()); if (editSettings) { TextInputLayoutHelper.configureAuthenticatedPasswordToggle( @@ -407,17 +408,14 @@ public class AccountSetupIncoming extends K9Activity implements OnClickListener * Shows/hides password field and client certificate spinner */ private void updateViewFromAuthType() { - AuthType authType = getSelectedAuthType(); - boolean isAuthTypeExternal = (AuthType.EXTERNAL == authType); - - if (isAuthTypeExternal) { - - // hide password fields, show client certificate fields - mPasswordLayoutView.setVisibility(View.GONE); - } else { - - // show password fields, hide client certificate fields - mPasswordLayoutView.setVisibility(View.VISIBLE); + switch (getSelectedAuthType()) { + case EXTERNAL: + case XOAUTH2: + mPasswordLayoutView.setVisibility(View.GONE); + break; + default: + mPasswordLayoutView.setVisibility(View.VISIBLE); + break; } } @@ -428,8 +426,9 @@ public class AccountSetupIncoming extends K9Activity implements OnClickListener private void updateViewFromSecurity() { ConnectionSecurity security = getSelectedSecurity(); boolean isUsingTLS = ((ConnectionSecurity.SSL_TLS_REQUIRED == security) || (ConnectionSecurity.STARTTLS_REQUIRED == security)); + boolean isUsingOAuth = getSelectedAuthType() == AuthType.XOAUTH2; - if (isUsingTLS) { + if (isUsingTLS && !isUsingOAuth) { mAllowClientCertificateView.setVisibility(View.VISIBLE); } else { mAllowClientCertificateView.setVisibility(View.GONE); @@ -500,9 +499,13 @@ public class AccountSetupIncoming extends K9Activity implements OnClickListener && hasConnectionSecurity && hasValidCertificateAlias; + boolean hasValidOAuthSettings = hasValidUserName + && hasConnectionSecurity + && authType == AuthType.XOAUTH2; + mNextButton.setEnabled(Utility.domainFieldValid(mServerView) && Utility.requiredFieldValid(mPortView) - && (hasValidPasswordSettings || hasValidExternalAuthSettings)); + && (hasValidPasswordSettings || hasValidExternalAuthSettings || hasValidOAuthSettings)); Utility.setCompoundDrawablesAlpha(mNextButton, mNextButton.isEnabled() ? 255 : 128); } diff --git a/app/ui/legacy/src/main/java/com/fsck/k9/activity/setup/AccountSetupOutgoing.java b/app/ui/legacy/src/main/java/com/fsck/k9/activity/setup/AccountSetupOutgoing.java index 8e64281ae7..7eba7c81e8 100644 --- a/app/ui/legacy/src/main/java/com/fsck/k9/activity/setup/AccountSetupOutgoing.java +++ b/app/ui/legacy/src/main/java/com/fsck/k9/activity/setup/AccountSetupOutgoing.java @@ -127,7 +127,7 @@ public class AccountSetupOutgoing extends K9Activity implements OnClickListener, mSecurityTypeView.setAdapter(ConnectionSecurityAdapter.get(this)); - mAuthTypeAdapter = AuthTypeAdapter.get(this); + mAuthTypeAdapter = AuthTypeAdapter.get(this, true); mAuthTypeView.setAdapter(mAuthTypeAdapter); /* @@ -162,8 +162,6 @@ public class AccountSetupOutgoing extends K9Activity implements OnClickListener, try { ServerSettings settings = mAccount.getOutgoingServerSettings(); - updateAuthPlainTextFromSecurityType(settings.connectionSecurity); - updateViewFromSecurity(settings.connectionSecurity); if (savedInstanceState == null) { // The first item is selected if settings.authenticationType is null or is not in mAuthTypeAdapter mCurrentAuthTypeViewPosition = mAuthTypeAdapter.getAuthPosition(settings.authenticationType); @@ -190,6 +188,9 @@ public class AccountSetupOutgoing extends K9Activity implements OnClickListener, } mSecurityTypeView.setSelection(mCurrentSecurityTypeViewPosition, false); + updateAuthPlainTextFromSecurityType(getSelectedSecurity()); + updateViewFromSecurity(); + if (!settings.username.isEmpty()) { mUsernameView.setText(settings.username); mRequireLoginView.setChecked(true); @@ -251,7 +252,7 @@ public class AccountSetupOutgoing extends K9Activity implements OnClickListener, */ if (mCurrentSecurityTypeViewPosition != position) { updatePortFromSecurityType(); - updateViewFromSecurity(getSelectedSecurity()); + updateViewFromSecurity(); boolean isInsecure = (ConnectionSecurity.NONE == getSelectedSecurity()); boolean isAuthExternal = (AuthType.EXTERNAL == getSelectedAuthType()); boolean loginNotRequired = !mRequireLoginView.isChecked(); @@ -292,6 +293,7 @@ public class AccountSetupOutgoing extends K9Activity implements OnClickListener, } updateViewFromAuthType(); + updateViewFromSecurity(); validateFields(); AuthType selection = getSelectedAuthType(); @@ -352,25 +354,26 @@ public class AccountSetupOutgoing extends K9Activity implements OnClickListener, * Shows/hides password field */ private void updateViewFromAuthType() { - AuthType authType = getSelectedAuthType(); - boolean isAuthTypeExternal = (AuthType.EXTERNAL == authType); - - if (isAuthTypeExternal) { - // hide password fields - mPasswordLayoutView.setVisibility(View.GONE); - } else { - // show password fields - mPasswordLayoutView.setVisibility(View.VISIBLE); + switch (getSelectedAuthType()) { + case EXTERNAL: + case XOAUTH2: + mPasswordLayoutView.setVisibility(View.GONE); + break; + default: + mPasswordLayoutView.setVisibility(View.VISIBLE); + break; } } /** * Shows/hides client certificate spinner */ - private void updateViewFromSecurity(ConnectionSecurity security) { + private void updateViewFromSecurity() { + ConnectionSecurity security = getSelectedSecurity(); boolean isUsingTLS = ((ConnectionSecurity.SSL_TLS_REQUIRED == security) || (ConnectionSecurity.STARTTLS_REQUIRED == security)); + boolean isUsingOAuth = getSelectedAuthType() == AuthType.XOAUTH2; - if (isUsingTLS) { + if (isUsingTLS && !isUsingOAuth) { mAllowClientCertificateView.setVisibility(View.VISIBLE); } else { mAllowClientCertificateView.setVisibility(View.GONE); @@ -406,7 +409,7 @@ public class AccountSetupOutgoing extends K9Activity implements OnClickListener, mAuthTypeView.setSelection(mCurrentAuthTypeViewPosition, false); mAuthTypeView.setOnItemSelectedListener(onItemSelectedListener); updateViewFromAuthType(); - updateViewFromSecurity(getSelectedSecurity()); + updateViewFromSecurity(); onItemSelectedListener = mSecurityTypeView.getOnItemSelectedListener(); mSecurityTypeView.setOnItemSelectedListener(null); @@ -441,11 +444,15 @@ public class AccountSetupOutgoing extends K9Activity implements OnClickListener, && hasConnectionSecurity && hasValidCertificateAlias; + boolean hasValidOAuthSettings = hasValidUserName + && hasConnectionSecurity + && authType == AuthType.XOAUTH2; + mNextButton .setEnabled(Utility.domainFieldValid(mServerView) && Utility.requiredFieldValid(mPortView) && (!mRequireLoginView.isChecked() - || hasValidPasswordSettings || hasValidExternalAuthSettings)); + || hasValidPasswordSettings || hasValidExternalAuthSettings || hasValidOAuthSettings)); Utility.setCompoundDrawablesAlpha(mNextButton, mNextButton.isEnabled() ? 255 : 128); } diff --git a/app/ui/legacy/src/main/java/com/fsck/k9/activity/setup/AuthTypeAdapter.java b/app/ui/legacy/src/main/java/com/fsck/k9/activity/setup/AuthTypeAdapter.java index 53a6d557ea..2df6c5e683 100644 --- a/app/ui/legacy/src/main/java/com/fsck/k9/activity/setup/AuthTypeAdapter.java +++ b/app/ui/legacy/src/main/java/com/fsck/k9/activity/setup/AuthTypeAdapter.java @@ -11,8 +11,14 @@ class AuthTypeAdapter extends ArrayAdapter { super(context, resource, holders); } - public static AuthTypeAdapter get(Context context) { - AuthType[] authTypes = new AuthType[]{AuthType.PLAIN, AuthType.CRAM_MD5, AuthType.EXTERNAL}; + public static AuthTypeAdapter get(Context context, boolean oAuthSupported) { + AuthType[] authTypes; + if (oAuthSupported) { + authTypes = new AuthType[] { AuthType.PLAIN, AuthType.CRAM_MD5, AuthType.EXTERNAL, AuthType.XOAUTH2 }; + } else { + authTypes = new AuthType[] { AuthType.PLAIN, AuthType.CRAM_MD5, AuthType.EXTERNAL }; + } + AuthTypeHolder[] holders = new AuthTypeHolder[authTypes.length]; for (int i = 0; i < authTypes.length; i++) { holders[i] = new AuthTypeHolder(authTypes[i], context.getResources()); diff --git a/app/ui/legacy/src/main/java/com/fsck/k9/activity/setup/AuthTypeHolder.java b/app/ui/legacy/src/main/java/com/fsck/k9/activity/setup/AuthTypeHolder.java index 998068d13e..a12739d55a 100644 --- a/app/ui/legacy/src/main/java/com/fsck/k9/activity/setup/AuthTypeHolder.java +++ b/app/ui/legacy/src/main/java/com/fsck/k9/activity/setup/AuthTypeHolder.java @@ -41,7 +41,8 @@ class AuthTypeHolder { return R.string.account_setup_auth_type_encrypted_password; case EXTERNAL: return R.string.account_setup_auth_type_tls_client_certificate; - + case XOAUTH2: + return R.string.account_setup_auth_type_oauth2; case AUTOMATIC: case LOGIN: default: diff --git a/app/ui/legacy/src/main/java/com/fsck/k9/activity/setup/AuthViewModel.kt b/app/ui/legacy/src/main/java/com/fsck/k9/activity/setup/AuthViewModel.kt new file mode 100644 index 0000000000..44e101e15b --- /dev/null +++ b/app/ui/legacy/src/main/java/com/fsck/k9/activity/setup/AuthViewModel.kt @@ -0,0 +1,269 @@ +package com.fsck.k9.activity.setup + +import android.app.Activity +import android.app.Application +import android.content.ActivityNotFoundException +import android.content.Context +import android.content.Intent +import android.net.Uri +import androidx.activity.result.ActivityResultLauncher +import androidx.activity.result.ActivityResultRegistry +import androidx.activity.result.contract.ActivityResultContract +import androidx.core.net.toUri +import androidx.lifecycle.AndroidViewModel +import androidx.lifecycle.DefaultLifecycleObserver +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.viewModelScope +import com.fsck.k9.Account +import com.fsck.k9.preferences.AccountManager +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import net.openid.appauth.AuthState +import net.openid.appauth.AuthorizationException +import net.openid.appauth.AuthorizationRequest +import net.openid.appauth.AuthorizationResponse +import net.openid.appauth.AuthorizationService +import net.openid.appauth.AuthorizationServiceConfiguration +import net.openid.appauth.ResponseTypeValues +import timber.log.Timber + +private const val KEY_AUTHORIZATION = "app.k9mail_auth" + +class AuthViewModel( + application: Application, + private val accountManager: AccountManager, + private val oauthCredentials: OAuthCredentials +) : AndroidViewModel(application) { + private var authService: AuthorizationService? = null + private val authState = AuthState() + + private var account: Account? = null + + private lateinit var resultObserver: AppAuthResultObserver + + private val _uiState = MutableStateFlow(AuthFlowState.Idle) + val uiState: StateFlow = _uiState.asStateFlow() + + @Synchronized + private fun getAuthService(): AuthorizationService { + return authService ?: AuthorizationService(getApplication()).also { authService = it } + } + + fun init(activityResultRegistry: ActivityResultRegistry, lifecycle: Lifecycle) { + resultObserver = AppAuthResultObserver(activityResultRegistry) + lifecycle.addObserver(resultObserver) + } + + fun authResultConsumed() { + _uiState.update { AuthFlowState.Idle } + } + + fun isAuthorized(account: Account): Boolean { + val authState = getOrCreateAuthState(account) + return authState.isAuthorized + } + + private fun getOrCreateAuthState(account: Account): AuthState { + return try { + account.oAuthState?.let { AuthState.jsonDeserialize(it) } ?: AuthState() + } catch (e: Exception) { + Timber.e(e, "Error deserializing AuthState") + AuthState() + } + } + + fun login(account: Account) { + this.account = account + + viewModelScope.launch { + val config = findOAuthConfiguration(account) + if (config == null) { + _uiState.update { AuthFlowState.NotSupported } + return@launch + } + + try { + startLogin(account, config) + } catch (e: ActivityNotFoundException) { + _uiState.update { AuthFlowState.BrowserNotFound } + } + } + } + + private suspend fun startLogin(account: Account, config: OAuthConfiguration) { + val authRequestIntent = withContext(Dispatchers.IO) { + createAuthorizationRequestIntent(account.email, config) + } + + resultObserver.login(authRequestIntent) + } + + private fun createAuthorizationRequestIntent(email: String, config: OAuthConfiguration): Intent { + val serviceConfig = AuthorizationServiceConfiguration( + config.authorizationEndpoint.toUri(), + config.tokenEndpoint.toUri() + ) + + val applicationId = getApplication().packageName + val redirectUri = Uri.parse("$applicationId:/oauth2redirect") + + val authRequestBuilder = AuthorizationRequest.Builder( + serviceConfig, + config.clientId, + ResponseTypeValues.CODE, + redirectUri + ) + + val scopeString = config.scopes.joinToString(separator = " ") + val authRequest = authRequestBuilder + .setScope(scopeString) + .setLoginHint(email) + .build() + + val authService = getAuthService() + + return authService.getAuthorizationRequestIntent(authRequest) + } + + private fun findOAuthConfiguration(account: Account): OAuthConfiguration? { + return when (account.incomingServerSettings.host) { + "imap.gmail.com", "imap.googlemail.com" -> { + OAuthConfiguration( + clientId = oauthCredentials.gmailClientId, + scopes = listOf("https://mail.google.com/"), + authorizationEndpoint = "https://accounts.google.com/o/oauth2/v2/auth", + tokenEndpoint = "https://oauth2.googleapis.com/token" + ) + } + else -> null + } + } + + private fun onLoginResult(authorizationResult: AuthorizationResult?) { + if (authorizationResult == null) { + _uiState.update { AuthFlowState.Canceled } + return + } + + authorizationResult.response?.let { response -> + authState.update(authorizationResult.response, authorizationResult.exception) + exchangeToken(response) + } + + authorizationResult.exception?.let { authorizationException -> + _uiState.update { + AuthFlowState.Failed( + errorCode = authorizationException.error, + errorMessage = authorizationException.errorDescription + ) + } + } + } + + private fun exchangeToken(response: AuthorizationResponse) { + viewModelScope.launch(Dispatchers.IO) { + val authService = getAuthService() + + val tokenRequest = response.createTokenExchangeRequest() + authService.performTokenRequest(tokenRequest) { tokenResponse, authorizationException -> + authState.update(tokenResponse, authorizationException) + + val account = account!! + account.oAuthState = authState.jsonSerializeString() + + viewModelScope.launch(Dispatchers.IO) { + accountManager.saveAccount(account) + } + + if (authorizationException != null) { + _uiState.update { + AuthFlowState.Failed( + errorCode = authorizationException.error, + errorMessage = authorizationException.errorDescription + ) + } + } else { + _uiState.update { AuthFlowState.Success } + } + } + } + } + + @Synchronized + override fun onCleared() { + authService?.dispose() + authService = null + } + + inner class AppAuthResultObserver(private val registry: ActivityResultRegistry) : DefaultLifecycleObserver { + private var authorizationLauncher: ActivityResultLauncher? = null + private var authRequestIntent: Intent? = null + + override fun onCreate(owner: LifecycleOwner) { + authorizationLauncher = registry.register(KEY_AUTHORIZATION, AuthorizationContract(), ::onLoginResult) + authRequestIntent?.let { intent -> + authRequestIntent = null + login(intent) + } + } + + override fun onDestroy(owner: LifecycleOwner) { + authorizationLauncher = null + } + + fun login(authRequestIntent: Intent) { + val launcher = authorizationLauncher + if (launcher != null) { + launcher.launch(authRequestIntent) + } else { + this.authRequestIntent = authRequestIntent + } + } + } +} + +private class AuthorizationContract : ActivityResultContract() { + override fun createIntent(context: Context, input: Intent): Intent { + return input + } + + override fun parseResult(resultCode: Int, intent: Intent?): AuthorizationResult? { + return if (resultCode == Activity.RESULT_OK && intent != null) { + AuthorizationResult( + response = AuthorizationResponse.fromIntent(intent), + exception = AuthorizationException.fromIntent(intent) + ) + } else { + null + } + } +} + +private data class AuthorizationResult( + val response: AuthorizationResponse?, + val exception: AuthorizationException? +) + +sealed interface AuthFlowState { + object Idle : AuthFlowState + + object Success : AuthFlowState + + object NotSupported : AuthFlowState + + object BrowserNotFound : AuthFlowState + + object Canceled : AuthFlowState + + data class Failed(val errorCode: String?, val errorMessage: String?) : AuthFlowState { + override fun toString(): String { + return listOfNotNull(errorCode, errorMessage).joinToString(separator = " - ") + } + } +} diff --git a/app/ui/legacy/src/main/java/com/fsck/k9/activity/setup/OAuthConfiguration.kt b/app/ui/legacy/src/main/java/com/fsck/k9/activity/setup/OAuthConfiguration.kt new file mode 100644 index 0000000000..0584046500 --- /dev/null +++ b/app/ui/legacy/src/main/java/com/fsck/k9/activity/setup/OAuthConfiguration.kt @@ -0,0 +1,8 @@ +package com.fsck.k9.activity.setup + +data class OAuthConfiguration( + val clientId: String, + val scopes: List, + val authorizationEndpoint: String, + val tokenEndpoint: String +) diff --git a/app/ui/legacy/src/main/java/com/fsck/k9/activity/setup/OAuthCredentials.kt b/app/ui/legacy/src/main/java/com/fsck/k9/activity/setup/OAuthCredentials.kt new file mode 100644 index 0000000000..23cdc6f894 --- /dev/null +++ b/app/ui/legacy/src/main/java/com/fsck/k9/activity/setup/OAuthCredentials.kt @@ -0,0 +1,5 @@ +package com.fsck.k9.activity.setup + +interface OAuthCredentials { + val gmailClientId: String +} diff --git a/app/ui/legacy/src/main/res/layout/account_setup_basics.xml b/app/ui/legacy/src/main/res/layout/account_setup_basics.xml index 5c425f88f4..f88622f7f4 100644 --- a/app/ui/legacy/src/main/res/layout/account_setup_basics.xml +++ b/app/ui/legacy/src/main/res/layout/account_setup_basics.xml @@ -37,6 +37,7 @@ diff --git a/app/ui/legacy/src/main/res/values/strings.xml b/app/ui/legacy/src/main/res/values/strings.xml index 54d46ca269..0382d61edd 100644 --- a/app/ui/legacy/src/main/res/values/strings.xml +++ b/app/ui/legacy/src/main/res/values/strings.xml @@ -355,6 +355,7 @@ Please submit bug reports, contribute new features and ask questions at Password, transmitted insecurely Encrypted password Client certificate + OAuth 2.0 Incoming server settings Username @@ -456,6 +457,10 @@ Please submit bug reports, contribute new features and ask questions at Username or password incorrect.\n(%s) The server presented an invalid SSL certificate. Sometimes, this is because of a server misconfiguration. Sometimes it is because someone is trying to attack you or your mail server. If you\'re not sure what\'s up, click Reject and contact the folks who manage your mail server.\n\n(%s) Cannot connect to server.\n(%s) + Authorization canceled + Authorization failed with the following error: %s + OAuth 2.0 is currently not supported with this provider. + The app couldn\'t find a browser to use for granting access to your account. Edit details Continue diff --git a/app/ui/legacy/src/test/AndroidManifest.xml b/app/ui/legacy/src/test/AndroidManifest.xml new file mode 100644 index 0000000000..9b1c30102c --- /dev/null +++ b/app/ui/legacy/src/test/AndroidManifest.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + diff --git a/debug.keystore b/debug.keystore new file mode 100644 index 0000000000000000000000000000000000000000..ddce9b12d43bced3500477f7724d6782a6273276 GIT binary patch literal 2651 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 jI getAccounts(); /** * Fetch a token. No guarantees are provided for validity. */ - String getToken(String username, long timeoutMillis) throws AuthenticationFailedException; + String getToken(long timeoutMillis) throws AuthenticationFailedException; /** * Invalidate the token for this username. @@ -32,5 +25,5 @@ public interface OAuth2TokenProvider { *

* Invalidating a token and then failure with a new token should be treated as a permanent failure. */ - void invalidateToken(String username); + void invalidateToken(); } diff --git a/mail/protocols/imap/src/main/java/com/fsck/k9/mail/store/imap/RealImapConnection.java b/mail/protocols/imap/src/main/java/com/fsck/k9/mail/store/imap/RealImapConnection.java index b949271e87..057dbdb6a0 100644 --- a/mail/protocols/imap/src/main/java/com/fsck/k9/mail/store/imap/RealImapConnection.java +++ b/mail/protocols/imap/src/main/java/com/fsck/k9/mail/store/imap/RealImapConnection.java @@ -399,7 +399,7 @@ class RealImapConnection implements ImapConnection { return attemptXOAuth2(); } catch (NegativeImapResponseException e) { //TODO: Check response code so we don't needlessly invalidate the token. - oauthTokenProvider.invalidateToken(settings.getUsername()); + oauthTokenProvider.invalidateToken(); if (!retryXoauth2WithNewToken) { throw handlePermanentXoauth2Failure(e); @@ -427,13 +427,13 @@ class RealImapConnection implements ImapConnection { //Okay, we failed on a new token. //Invalidate the token anyway but assume it's permanent. Timber.v(e, "Authentication exception for new token, permanent error assumed"); - oauthTokenProvider.invalidateToken(settings.getUsername()); + oauthTokenProvider.invalidateToken(); throw handlePermanentXoauth2Failure(e2); } } private List attemptXOAuth2() throws MessagingException, IOException { - String token = oauthTokenProvider.getToken(settings.getUsername(), OAuth2TokenProvider.OAUTH2_TIMEOUT); + String token = oauthTokenProvider.getToken(OAuth2TokenProvider.OAUTH2_TIMEOUT); String authString = Authentication.computeXoauth(settings.getUsername(), token); String tag = sendSaslIrCommand(Commands.AUTHENTICATE_XOAUTH2, authString, true); diff --git a/mail/protocols/imap/src/test/java/com/fsck/k9/mail/store/imap/RealImapConnectionTest.kt b/mail/protocols/imap/src/test/java/com/fsck/k9/mail/store/imap/RealImapConnectionTest.kt index eb86e91647..99bb4f4563 100644 --- a/mail/protocols/imap/src/test/java/com/fsck/k9/mail/store/imap/RealImapConnectionTest.kt +++ b/mail/protocols/imap/src/test/java/com/fsck/k9/mail/store/imap/RealImapConnectionTest.kt @@ -1045,8 +1045,7 @@ class RealImapConnectionTest { class TestTokenProvider : OAuth2TokenProvider { private var invalidationCount = 0 - override fun getToken(username: String, timeoutMillis: Long): String { - assertThat(username).isEqualTo(USERNAME) + override fun getToken(timeoutMillis: Long): String { assertThat(timeoutMillis).isEqualTo(OAuth2TokenProvider.OAUTH2_TIMEOUT.toLong()) return when (invalidationCount) { @@ -1060,14 +1059,9 @@ class TestTokenProvider : OAuth2TokenProvider { } } - override fun invalidateToken(username: String) { - assertThat(username).isEqualTo(USERNAME) + override fun invalidateToken() { invalidationCount++ } - - override fun getAccounts(): List { - throw UnsupportedOperationException() - } } private fun String.base64() = this.encodeUtf8().base64() diff --git a/mail/protocols/smtp/src/main/java/com/fsck/k9/mail/transport/smtp/SmtpTransport.kt b/mail/protocols/smtp/src/main/java/com/fsck/k9/mail/transport/smtp/SmtpTransport.kt index 30fd795c5a..9ed38fd824 100644 --- a/mail/protocols/smtp/src/main/java/com/fsck/k9/mail/transport/smtp/SmtpTransport.kt +++ b/mail/protocols/smtp/src/main/java/com/fsck/k9/mail/transport/smtp/SmtpTransport.kt @@ -559,7 +559,7 @@ class SmtpTransport( throw negativeResponse } - oauthTokenProvider!!.invalidateToken(username) + oauthTokenProvider!!.invalidateToken() if (!retryXoauthWithNewToken) { handlePermanentFailure(negativeResponse) @@ -588,13 +588,13 @@ class SmtpTransport( // Okay, we failed on a new token. Invalidate the token anyway but assume it's permanent. Timber.v(negativeResponseFromNewToken, "Authentication exception for new token, permanent error assumed") - oauthTokenProvider!!.invalidateToken(username) + oauthTokenProvider!!.invalidateToken() handlePermanentFailure(negativeResponseFromNewToken) } } private fun attemptXoauth2(username: String) { - val token = oauthTokenProvider!!.getToken(username, OAuth2TokenProvider.OAUTH2_TIMEOUT.toLong()) + val token = oauthTokenProvider!!.getToken(OAuth2TokenProvider.OAUTH2_TIMEOUT.toLong()) val authString = Authentication.computeXoauth(username, token) val response = executeSensitiveCommand("AUTH XOAUTH2 %s", authString) diff --git a/mail/protocols/smtp/src/test/java/com/fsck/k9/mail/transport/smtp/SmtpTransportTest.kt b/mail/protocols/smtp/src/test/java/com/fsck/k9/mail/transport/smtp/SmtpTransportTest.kt index be50836875..7819737d98 100644 --- a/mail/protocols/smtp/src/test/java/com/fsck/k9/mail/transport/smtp/SmtpTransportTest.kt +++ b/mail/protocols/smtp/src/test/java/com/fsck/k9/mail/transport/smtp/SmtpTransportTest.kt @@ -18,10 +18,8 @@ import com.google.common.truth.Truth.assertThat import org.junit.Assert.fail import org.junit.Test import org.mockito.ArgumentMatchers.anyLong -import org.mockito.ArgumentMatchers.anyString import org.mockito.kotlin.doReturn import org.mockito.kotlin.doThrow -import org.mockito.kotlin.eq import org.mockito.kotlin.inOrder import org.mockito.kotlin.mock import org.mockito.kotlin.stubbing @@ -218,8 +216,8 @@ class SmtpTransportTest { } inOrder(oAuth2TokenProvider) { - verify(oAuth2TokenProvider).getToken(eq(USERNAME), anyLong()) - verify(oAuth2TokenProvider).invalidateToken(USERNAME) + verify(oAuth2TokenProvider).getToken(anyLong()) + verify(oAuth2TokenProvider).invalidateToken() } server.verifyConnectionClosed() server.verifyInteractionCompleted() @@ -245,9 +243,9 @@ class SmtpTransportTest { transport.open() inOrder(oAuth2TokenProvider) { - verify(oAuth2TokenProvider).getToken(eq(USERNAME), anyLong()) - verify(oAuth2TokenProvider).invalidateToken(USERNAME) - verify(oAuth2TokenProvider).getToken(eq(USERNAME), anyLong()) + verify(oAuth2TokenProvider).getToken(anyLong()) + verify(oAuth2TokenProvider).invalidateToken() + verify(oAuth2TokenProvider).getToken(anyLong()) } server.verifyConnectionStillOpen() server.verifyInteractionCompleted() @@ -273,9 +271,9 @@ class SmtpTransportTest { transport.open() inOrder(oAuth2TokenProvider) { - verify(oAuth2TokenProvider).getToken(eq(USERNAME), anyLong()) - verify(oAuth2TokenProvider).invalidateToken(USERNAME) - verify(oAuth2TokenProvider).getToken(eq(USERNAME), anyLong()) + verify(oAuth2TokenProvider).getToken(anyLong()) + verify(oAuth2TokenProvider).invalidateToken() + verify(oAuth2TokenProvider).getToken(anyLong()) } server.verifyConnectionStillOpen() server.verifyInteractionCompleted() @@ -301,9 +299,9 @@ class SmtpTransportTest { transport.open() inOrder(oAuth2TokenProvider) { - verify(oAuth2TokenProvider).getToken(eq(USERNAME), anyLong()) - verify(oAuth2TokenProvider).invalidateToken(USERNAME) - verify(oAuth2TokenProvider).getToken(eq(USERNAME), anyLong()) + verify(oAuth2TokenProvider).getToken(anyLong()) + verify(oAuth2TokenProvider).invalidateToken() + verify(oAuth2TokenProvider).getToken(anyLong()) } server.verifyConnectionStillOpen() server.verifyInteractionCompleted() @@ -357,7 +355,7 @@ class SmtpTransportTest { output("221 BYE") } stubbing(oAuth2TokenProvider) { - on { getToken(anyString(), anyLong()) } doThrow AuthenticationFailedException("Failed to fetch token") + on { getToken(anyLong()) } doThrow AuthenticationFailedException("Failed to fetch token") } val transport = startServerAndCreateSmtpTransport(server, authenticationType = AuthType.XOAUTH2) @@ -533,8 +531,8 @@ class SmtpTransportTest { } inOrder(oAuth2TokenProvider) { - verify(oAuth2TokenProvider).getToken(eq(USERNAME), anyLong()) - verify(oAuth2TokenProvider).invalidateToken(USERNAME) + verify(oAuth2TokenProvider).getToken(anyLong()) + verify(oAuth2TokenProvider).invalidateToken() } server.verifyConnectionClosed() server.verifyInteractionCompleted() @@ -950,7 +948,7 @@ class SmtpTransportTest { private fun createMockOAuth2TokenProvider(): OAuth2TokenProvider { return mock { - on { getToken(eq(USERNAME), anyLong()) } doReturn "oldToken" doReturn "newToken" + on { getToken(anyLong()) } doReturn "oldToken" doReturn "newToken" } } } -- GitLab From f0535c62173f734356832c02b8f2597ecbbffd1c Mon Sep 17 00:00:00 2001 From: Mihail Mitrofanov Date: Tue, 24 May 2022 15:35:57 +0200 Subject: [PATCH 58/75] The back button now appears in AccountSetupIncoming and AccountSetupOutgoing only in edit mode * Condition Intent.ACTION_EDIT.equals(getIntent().getAction()); --- .../com/fsck/k9/activity/setup/AccountSetupIncoming.java | 8 ++++---- .../com/fsck/k9/activity/setup/AccountSetupOutgoing.java | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/app/ui/legacy/src/main/java/com/fsck/k9/activity/setup/AccountSetupIncoming.java b/app/ui/legacy/src/main/java/com/fsck/k9/activity/setup/AccountSetupIncoming.java index 8cea8f712e..1a01df742d 100644 --- a/app/ui/legacy/src/main/java/com/fsck/k9/activity/setup/AccountSetupIncoming.java +++ b/app/ui/legacy/src/main/java/com/fsck/k9/activity/setup/AccountSetupIncoming.java @@ -118,10 +118,6 @@ public class AccountSetupIncoming extends K9Activity implements OnClickListener setLayout(R.layout.account_setup_incoming); setTitle(R.string.account_setup_incoming_title); - if (getSupportActionBar() != null) { - getSupportActionBar().setDisplayHomeAsUpEnabled(true); - } - mUsernameView = findViewById(R.id.account_username); mPasswordView = findViewById(R.id.account_password); mClientCertificateSpinner = findViewById(R.id.account_client_certificate_spinner); @@ -186,6 +182,10 @@ public class AccountSetupIncoming extends K9Activity implements OnClickListener getString(R.string.account_setup_basics_show_password_biometrics_subtitle), getString(R.string.account_setup_basics_show_password_need_lock) ); + + if (getSupportActionBar() != null) { + getSupportActionBar().setDisplayHomeAsUpEnabled(true); + } } try { diff --git a/app/ui/legacy/src/main/java/com/fsck/k9/activity/setup/AccountSetupOutgoing.java b/app/ui/legacy/src/main/java/com/fsck/k9/activity/setup/AccountSetupOutgoing.java index 1173e71f60..c605511bae 100644 --- a/app/ui/legacy/src/main/java/com/fsck/k9/activity/setup/AccountSetupOutgoing.java +++ b/app/ui/legacy/src/main/java/com/fsck/k9/activity/setup/AccountSetupOutgoing.java @@ -100,10 +100,6 @@ public class AccountSetupOutgoing extends K9Activity implements OnClickListener, setLayout(R.layout.account_setup_outgoing); setTitle(R.string.account_setup_outgoing_title); - if (getSupportActionBar() != null) { - getSupportActionBar().setDisplayHomeAsUpEnabled(true); - } - String accountUuid = getIntent().getStringExtra(EXTRA_ACCOUNT); mAccount = Preferences.getPreferences(this).getAccount(accountUuid); @@ -162,6 +158,10 @@ public class AccountSetupOutgoing extends K9Activity implements OnClickListener, getString(R.string.account_setup_basics_show_password_biometrics_subtitle), getString(R.string.account_setup_basics_show_password_need_lock) ); + + if (getSupportActionBar() != null) { + getSupportActionBar().setDisplayHomeAsUpEnabled(true); + } } try { -- GitLab From 6081b582123b564f9946419f40721e1d44a56f80 Mon Sep 17 00:00:00 2001 From: Sylvia van Os Date: Sun, 1 May 2022 22:45:53 +0200 Subject: [PATCH 59/75] Add Unsubscribe toolbar option --- .../fsck/k9/helper/ListUnsubscribeHelper.kt | 55 +++++++++++ .../java/com/fsck/k9/helper/UnsubscribeUri.kt | 10 ++ .../fsck/k9/mailstore/MessageViewInfo.java | 20 ++-- .../mailstore/MessageViewInfoExtractor.java | 7 +- .../k9/helper/ListUnsubscribeHelperTest.kt | 95 +++++++++++++++++++ .../java/com/fsck/k9/activity/MessageList.kt | 6 ++ .../ui/messageview/MessageViewFragment.java | 23 +++++ .../src/main/res/menu/message_list_option.xml | 4 + app/ui/legacy/src/main/res/values/strings.xml | 1 + .../fsck/k9/mail/store/imap/RealImapFolder.kt | 2 +- .../k9/mail/store/imap/RealImapFolderTest.kt | 2 +- 11 files changed, 215 insertions(+), 10 deletions(-) create mode 100644 app/core/src/main/java/com/fsck/k9/helper/ListUnsubscribeHelper.kt create mode 100644 app/core/src/main/java/com/fsck/k9/helper/UnsubscribeUri.kt create mode 100644 app/core/src/test/java/com/fsck/k9/helper/ListUnsubscribeHelperTest.kt diff --git a/app/core/src/main/java/com/fsck/k9/helper/ListUnsubscribeHelper.kt b/app/core/src/main/java/com/fsck/k9/helper/ListUnsubscribeHelper.kt new file mode 100644 index 0000000000..0809026537 --- /dev/null +++ b/app/core/src/main/java/com/fsck/k9/helper/ListUnsubscribeHelper.kt @@ -0,0 +1,55 @@ +package com.fsck.k9.helper + +import android.net.Uri +import com.fsck.k9.mail.Message +import java.util.regex.Pattern + +object ListUnsubscribeHelper { + private const val LIST_UNSUBSCRIBE_HEADER = "List-Unsubscribe" + private val MAILTO_CONTAINER_PATTERN = Pattern.compile("<(mailto:.+?)>") + private val HTTPS_CONTAINER_PATTERN = Pattern.compile("<(https:.+?)>") + + // As K-9 Mail is an email client, we prefer a mailto: unsubscribe method + // but if none is found, a https URL is acceptable too + fun getPreferredListUnsubscribeUri(message: Message): UnsubscribeUri? { + val headerValues = message.getHeader(LIST_UNSUBSCRIBE_HEADER) + if (headerValues.isEmpty()) { + return null + } + val listUnsubscribeUris = mutableListOf() + for (headerValue in headerValues) { + val uri = extractUri(headerValue) ?: continue + + if (uri.scheme == "mailto") { + return MailtoUnsubscribeUri(uri) + } + + // If we got here it must be HTTPS + listUnsubscribeUris.add(uri) + } + + if (listUnsubscribeUris.isNotEmpty()) { + return HttpsUnsubscribeUri(listUnsubscribeUris[0]) + } + + return null + } + + private fun extractUri(headerValue: String?): Uri? { + if (headerValue == null || headerValue.isEmpty()) { + return null + } + + var matcher = MAILTO_CONTAINER_PATTERN.matcher(headerValue) + if (matcher.find()) { + return Uri.parse(matcher.group(1)) + } + + matcher = HTTPS_CONTAINER_PATTERN.matcher(headerValue) + if (matcher.find()) { + return Uri.parse(matcher.group(1)) + } + + return null + } +} diff --git a/app/core/src/main/java/com/fsck/k9/helper/UnsubscribeUri.kt b/app/core/src/main/java/com/fsck/k9/helper/UnsubscribeUri.kt new file mode 100644 index 0000000000..d335d2edd1 --- /dev/null +++ b/app/core/src/main/java/com/fsck/k9/helper/UnsubscribeUri.kt @@ -0,0 +1,10 @@ +package com.fsck.k9.helper + +import android.net.Uri + +sealed interface UnsubscribeUri { + val uri: Uri +} + +data class MailtoUnsubscribeUri(override val uri: Uri) : UnsubscribeUri +data class HttpsUnsubscribeUri(override val uri: Uri) : UnsubscribeUri diff --git a/app/core/src/main/java/com/fsck/k9/mailstore/MessageViewInfo.java b/app/core/src/main/java/com/fsck/k9/mailstore/MessageViewInfo.java index 74e66529b8..ed4e39093c 100644 --- a/app/core/src/main/java/com/fsck/k9/mailstore/MessageViewInfo.java +++ b/app/core/src/main/java/com/fsck/k9/mailstore/MessageViewInfo.java @@ -4,6 +4,7 @@ package com.fsck.k9.mailstore; import java.util.Collections; import java.util.List; +import com.fsck.k9.helper.UnsubscribeUri; import com.fsck.k9.mail.Body; import com.fsck.k9.mail.Message; import com.fsck.k9.mail.MessagingException; @@ -24,6 +25,7 @@ public class MessageViewInfo { public final List attachments; public final String extraText; public final List extraAttachments; + public final UnsubscribeUri preferredUnsubscribeUri; public MessageViewInfo( @@ -32,7 +34,8 @@ public class MessageViewInfo { String text, List attachments, CryptoResultAnnotation cryptoResultAnnotation, AttachmentResolver attachmentResolver, - String extraText, List extraAttachments) { + String extraText, List extraAttachments, + UnsubscribeUri preferredUnsubscribeUri) { this.message = message; this.isMessageIncomplete = isMessageIncomplete; this.rootPart = rootPart; @@ -44,13 +47,15 @@ public class MessageViewInfo { this.attachments = attachments; this.extraText = extraText; this.extraAttachments = extraAttachments; + this.preferredUnsubscribeUri = preferredUnsubscribeUri; } static MessageViewInfo createWithExtractedContent(Message message, Part rootPart, boolean isMessageIncomplete, - String text, List attachments, AttachmentResolver attachmentResolver) { + String text, List attachments, AttachmentResolver attachmentResolver, + UnsubscribeUri preferredUnsubscribeUri) { return new MessageViewInfo( message, isMessageIncomplete, rootPart, null, false, text, attachments, null, attachmentResolver, null, - Collections.emptyList()); + Collections.emptyList(), preferredUnsubscribeUri); } public static MessageViewInfo createWithErrorState(Message message, boolean isMessageIncomplete) { @@ -59,7 +64,7 @@ public class MessageViewInfo { Part emptyPart = new MimeBodyPart(emptyBody, "text/plain"); String subject = message.getSubject(); return new MessageViewInfo(message, isMessageIncomplete, emptyPart, subject, false, null, null, null, null, - null, null); + null, null, null); } catch (MessagingException e) { throw new AssertionError(e); } @@ -67,7 +72,7 @@ public class MessageViewInfo { public static MessageViewInfo createForMetadataOnly(Message message, boolean isMessageIncomplete) { String subject = message.getSubject(); - return new MessageViewInfo(message, isMessageIncomplete, null, subject, false, null, null, null, null, null, null); + return new MessageViewInfo(message, isMessageIncomplete, null, subject, false, null, null, null, null, null, null, null); } MessageViewInfo withCryptoData(CryptoResultAnnotation rootPartAnnotation, String extraViewableText, @@ -76,14 +81,15 @@ public class MessageViewInfo { message, isMessageIncomplete, rootPart, subject, isSubjectEncrypted, text, attachments, rootPartAnnotation, attachmentResolver, - extraViewableText, extraAttachmentInfos + extraViewableText, extraAttachmentInfos, + preferredUnsubscribeUri ); } MessageViewInfo withSubject(String subject, boolean isSubjectEncrypted) { return new MessageViewInfo( message, isMessageIncomplete, rootPart, subject, isSubjectEncrypted, text, attachments, - cryptoResultAnnotation, attachmentResolver, extraText, extraAttachments + cryptoResultAnnotation, attachmentResolver, extraText, extraAttachments, preferredUnsubscribeUri ); } } diff --git a/app/core/src/main/java/com/fsck/k9/mailstore/MessageViewInfoExtractor.java b/app/core/src/main/java/com/fsck/k9/mailstore/MessageViewInfoExtractor.java index dc18704e76..b98c801464 100644 --- a/app/core/src/main/java/com/fsck/k9/mailstore/MessageViewInfoExtractor.java +++ b/app/core/src/main/java/com/fsck/k9/mailstore/MessageViewInfoExtractor.java @@ -12,6 +12,8 @@ import androidx.annotation.WorkerThread; import com.fsck.k9.CoreResourceProvider; import com.fsck.k9.crypto.MessageCryptoStructureDetector; +import com.fsck.k9.helper.ListUnsubscribeHelper; +import com.fsck.k9.helper.UnsubscribeUri; import com.fsck.k9.mail.Address; import com.fsck.k9.mail.Flag; import com.fsck.k9.mail.Message; @@ -144,8 +146,11 @@ public class MessageViewInfoExtractor { boolean isMessageIncomplete = !message.isSet(Flag.X_DOWNLOADED_FULL) || MessageExtractor.hasMissingParts(message); + UnsubscribeUri preferredUnsubscribeUri = ListUnsubscribeHelper.INSTANCE.getPreferredListUnsubscribeUri(message); + return MessageViewInfo.createWithExtractedContent( - message, contentPart, isMessageIncomplete, viewable.html, attachmentInfos, attachmentResolver); + message, contentPart, isMessageIncomplete, viewable.html, attachmentInfos, attachmentResolver, + preferredUnsubscribeUri); } private ViewableExtractedText extractViewableAndAttachments(List parts, diff --git a/app/core/src/test/java/com/fsck/k9/helper/ListUnsubscribeHelperTest.kt b/app/core/src/test/java/com/fsck/k9/helper/ListUnsubscribeHelperTest.kt new file mode 100644 index 0000000000..4c327aefe1 --- /dev/null +++ b/app/core/src/test/java/com/fsck/k9/helper/ListUnsubscribeHelperTest.kt @@ -0,0 +1,95 @@ +package com.fsck.k9.helper + +import androidx.core.net.toUri +import com.fsck.k9.RobolectricTest +import com.fsck.k9.mail.internet.MimeMessage +import com.google.common.truth.Truth.assertThat +import kotlin.test.assertNull +import org.junit.Test + +class ListUnsubscribeHelperTest : RobolectricTest() { + @Test + fun `get list unsubscribe url - should accept mailto`() { + val message = buildMimeMessageWithListUnsubscribeValue( + "" + ) + val result = ListUnsubscribeHelper.getPreferredListUnsubscribeUri(message) + assertThat(result).isEqualTo(MailtoUnsubscribeUri("mailto:unsubscribe@example.com".toUri())) + } + + @Test + fun `get list unsubscribe url - should prefer mailto 1`() { + val message = buildMimeMessageWithListUnsubscribeValue( + ", " + ) + val result = ListUnsubscribeHelper.getPreferredListUnsubscribeUri(message) + assertThat(result).isEqualTo(MailtoUnsubscribeUri("mailto:unsubscribe@example.com".toUri())) + } + + @Test + fun `get list unsubscribe url - should prefer mailto 2`() { + val message = buildMimeMessageWithListUnsubscribeValue( + ", " + ) + val result = ListUnsubscribeHelper.getPreferredListUnsubscribeUri(message) + assertThat(result).isEqualTo(MailtoUnsubscribeUri("mailto:unsubscribe@example.com".toUri())) + } + + @Test + fun `get list unsubscribe url - should allow https if no mailto`() { + val message = buildMimeMessageWithListUnsubscribeValue( + "" + ) + val result = ListUnsubscribeHelper.getPreferredListUnsubscribeUri(message) + assertThat(result).isEqualTo(HttpsUnsubscribeUri("https://example.com/unsubscribe".toUri())) + } + + @Test + fun `get list unsubscribe url - should correctly parse uncommon urls`() { + val message = buildMimeMessageWithListUnsubscribeValue( + "" + ) + val result = ListUnsubscribeHelper.getPreferredListUnsubscribeUri(message) + assertThat(result).isEqualTo(HttpsUnsubscribeUri("https://domain.example/one,two".toUri())) + } + + @Test + fun `get list unsubscribe url - should ignore unsafe entries 1`() { + val message = buildMimeMessageWithListUnsubscribeValue( + "" + ) + val result = ListUnsubscribeHelper.getPreferredListUnsubscribeUri(message) + assertNull(result) + } + + @Test + fun `get list unsubscribe url - should ignore unsafe entries 2`() { + val message = buildMimeMessageWithListUnsubscribeValue( + ", " + ) + val result = ListUnsubscribeHelper.getPreferredListUnsubscribeUri(message) + assertThat(result).isEqualTo(HttpsUnsubscribeUri("https://example.com/unsubscribe".toUri())) + } + + @Test + fun `get list unsubscribe url - should ignore empty`() { + val message = buildMimeMessageWithListUnsubscribeValue( + "" + ) + val result = ListUnsubscribeHelper.getPreferredListUnsubscribeUri(message) + assertNull(result) + } + + @Test + fun `get list unsubscribe url - should ignore missing header`() { + val message = MimeMessage() + val result = ListUnsubscribeHelper.getPreferredListUnsubscribeUri(message) + assertNull(result) + } + + private fun buildMimeMessageWithListUnsubscribeValue(value: String): MimeMessage { + val message = MimeMessage() + message.addHeader("List-Unsubscribe", value) + return message + } +} diff --git a/app/ui/legacy/src/main/java/com/fsck/k9/activity/MessageList.kt b/app/ui/legacy/src/main/java/com/fsck/k9/activity/MessageList.kt index 579d00cd9b..a9991c6412 100644 --- a/app/ui/legacy/src/main/java/com/fsck/k9/activity/MessageList.kt +++ b/app/ui/legacy/src/main/java/com/fsck/k9/activity/MessageList.kt @@ -999,6 +999,9 @@ open class MessageList : } else if (id == R.id.move_to_drafts) { messageViewFragment!!.onMoveToDrafts() return true + } else if (id == R.id.unsubscribe) { + messageViewFragment!!.onUnsubscribe() + return true } else if (id == R.id.show_headers) { startActivity(MessageSourceActivity.createLaunchIntent(this, messageViewFragment!!.messageReference)) return true @@ -1094,6 +1097,7 @@ open class MessageList : menu.findItem(R.id.refile).isVisible = false menu.findItem(R.id.toggle_unread).isVisible = false menu.findItem(R.id.toggle_message_view_theme).isVisible = false + menu.findItem(R.id.unsubscribe).isVisible = false menu.findItem(R.id.show_headers).isVisible = false } else { // hide prev/next buttons in split mode @@ -1176,6 +1180,8 @@ open class MessageList : if (messageViewFragment!!.isOutbox) { menu.findItem(R.id.move_to_drafts).isVisible = true } + + menu.findItem(R.id.unsubscribe).isVisible = messageViewFragment!!.canMessageBeUnsubscribed() } // Set visibility of menu items related to the message list diff --git a/app/ui/legacy/src/main/java/com/fsck/k9/ui/messageview/MessageViewFragment.java b/app/ui/legacy/src/main/java/com/fsck/k9/ui/messageview/MessageViewFragment.java index d38cbab8c1..e1ce84d49d 100644 --- a/app/ui/legacy/src/main/java/com/fsck/k9/ui/messageview/MessageViewFragment.java +++ b/app/ui/legacy/src/main/java/com/fsck/k9/ui/messageview/MessageViewFragment.java @@ -36,6 +36,9 @@ import com.fsck.k9.Account; import com.fsck.k9.DI; import com.fsck.k9.K9; import com.fsck.k9.Preferences; +import com.fsck.k9.activity.MessageCompose; +import com.fsck.k9.helper.MailtoUnsubscribeUri; +import com.fsck.k9.helper.UnsubscribeUri; import com.fsck.k9.ui.choosefolder.ChooseFolderActivity; import com.fsck.k9.activity.MessageLoaderHelper; import com.fsck.k9.activity.MessageLoaderHelper.MessageLoaderCallbacks; @@ -96,6 +99,7 @@ public class MessageViewFragment extends Fragment implements ConfirmationDialogF private MessageLoaderHelper messageLoaderHelper; private MessageCryptoPresenter messageCryptoPresenter; private Long showProgressThreshold; + private UnsubscribeUri preferredUnsubscribeUri; /** * Used to temporarily store the destination folder for refile operations if a confirmation @@ -655,6 +659,23 @@ public class MessageViewFragment extends Fragment implements ConfirmationDialogF return mMessageReference.getFolderId() != spamFolderId; } + public boolean canMessageBeUnsubscribed() { + return preferredUnsubscribeUri != null; + } + + public void onUnsubscribe() { + if (preferredUnsubscribeUri instanceof MailtoUnsubscribeUri) { + Intent intent = new Intent(mContext, MessageCompose.class); + intent.setAction(Intent.ACTION_VIEW); + intent.setData(preferredUnsubscribeUri.getUri()); + intent.putExtra(MessageCompose.EXTRA_ACCOUNT, mMessageReference.getAccountUuid()); + startActivity(intent); + } else { + Intent intent = new Intent(Intent.ACTION_VIEW, preferredUnsubscribeUri.getUri()); + startActivity(intent); + } + } + public Context getApplicationContext() { return mContext; } @@ -779,12 +800,14 @@ public class MessageViewFragment extends Fragment implements ConfirmationDialogF @Override public void onMessageViewInfoLoadFinished(MessageViewInfo messageViewInfo) { showMessage(messageViewInfo); + preferredUnsubscribeUri = messageViewInfo.preferredUnsubscribeUri; showProgressThreshold = null; } @Override public void onMessageViewInfoLoadFailed(MessageViewInfo messageViewInfo) { showMessage(messageViewInfo); + preferredUnsubscribeUri = null; showProgressThreshold = null; } diff --git a/app/ui/legacy/src/main/res/menu/message_list_option.xml b/app/ui/legacy/src/main/res/menu/message_list_option.xml index 04e2462797..db167ddc63 100644 --- a/app/ui/legacy/src/main/res/menu/message_list_option.xml +++ b/app/ui/legacy/src/main/res/menu/message_list_option.xml @@ -147,6 +147,10 @@ + + diff --git a/app/ui/legacy/src/main/res/values/strings.xml b/app/ui/legacy/src/main/res/values/strings.xml index 54d46ca269..739209ade4 100644 --- a/app/ui/legacy/src/main/res/values/strings.xml +++ b/app/ui/legacy/src/main/res/values/strings.xml @@ -137,6 +137,7 @@ Please submit bug reports, contribute new features and ask questions at Add star Remove star Copy + Unsubscribe Show headers Address copied to clipboard 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 2230ea4c64..9508716b77 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 @@ -538,7 +538,7 @@ internal class RealImapFolder( fetchFields.add("RFC822.SIZE") fetchFields.add( "BODY.PEEK[HEADER.FIELDS (date subject from content-type to cc " + - "reply-to message-id references in-reply-to " + + "reply-to message-id references in-reply-to list-unsubscribe " + K9MailLib.IDENTITY_HEADER + " " + K9MailLib.CHAT_HEADER + ")]" ) } 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 558ff33e05..1fea9ae2ca 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 @@ -697,7 +697,7 @@ class RealImapFolderTest { verify(imapConnection).sendCommand( "UID FETCH 1 (UID INTERNALDATE RFC822.SIZE BODY.PEEK[HEADER.FIELDS " + - "(date subject from content-type to cc reply-to message-id references in-reply-to " + + "(date subject from content-type to cc reply-to message-id references in-reply-to list-unsubscribe " + "X-K9mail-Identity Chat-Version)])", false ) -- GitLab From a1936435911c9c2c21a6a9335b5be5305f15bc0f Mon Sep 17 00:00:00 2001 From: cketti Date: Sat, 28 May 2022 14:09:38 +0200 Subject: [PATCH 60/75] Update `AttachmentPresenterTest` --- .../fsck/k9/activity/compose/AttachmentPresenterTest.kt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/ui/legacy/src/test/java/com/fsck/k9/activity/compose/AttachmentPresenterTest.kt b/app/ui/legacy/src/test/java/com/fsck/k9/activity/compose/AttachmentPresenterTest.kt index d0b965eb1b..6a1f04a0c5 100644 --- a/app/ui/legacy/src/test/java/com/fsck/k9/activity/compose/AttachmentPresenterTest.kt +++ b/app/ui/legacy/src/test/java/com/fsck/k9/activity/compose/AttachmentPresenterTest.kt @@ -62,7 +62,7 @@ class AttachmentPresenterTest : K9RobolectricTest() { ) val messageViewInfo = MessageViewInfo( message, false, message, SUBJECT, false, TEXT, listOf(attachmentViewInfo), null, attachmentResolver, - EXTRA_TEXT, ArrayList() + EXTRA_TEXT, ArrayList(), null ) mockLoaderManager({ attachmentPresenter.attachments.get(0) as Attachment }) @@ -90,7 +90,7 @@ class AttachmentPresenterTest : K9RobolectricTest() { ) val messageViewInfo = MessageViewInfo( message, false, message, SUBJECT, false, TEXT, listOf(attachmentViewInfo), null, attachmentResolver, - EXTRA_TEXT, ArrayList() + EXTRA_TEXT, ArrayList(), null ) val result = attachmentPresenter.loadAllAvailableAttachments(messageViewInfo) @@ -111,7 +111,7 @@ class AttachmentPresenterTest : K9RobolectricTest() { val attachmentViewInfo = AttachmentViewInfo(MIME_TYPE, ATTACHMENT_NAME, size, URI, true, localBodyPart, true) val messageViewInfo = MessageViewInfo( message, false, message, SUBJECT, false, TEXT, listOf(attachmentViewInfo), null, attachmentResolver, - EXTRA_TEXT, ArrayList() + EXTRA_TEXT, ArrayList(), null ) mockLoaderManager({ attachmentPresenter.inlineAttachments.get(contentId) as Attachment }) @@ -139,7 +139,7 @@ class AttachmentPresenterTest : K9RobolectricTest() { val attachmentViewInfo = AttachmentViewInfo(MIME_TYPE, ATTACHMENT_NAME, size, URI, true, localBodyPart, false) val messageViewInfo = MessageViewInfo( message, false, message, SUBJECT, false, TEXT, listOf(attachmentViewInfo), null, attachmentResolver, - EXTRA_TEXT, ArrayList() + EXTRA_TEXT, ArrayList(), null ) val result = attachmentPresenter.loadAllAvailableAttachments(messageViewInfo) -- GitLab From d4883d19211baca098ac12450a17c0f5479d12d3 Mon Sep 17 00:00:00 2001 From: cketti Date: Fri, 3 Jun 2022 23:25:58 +0200 Subject: [PATCH 61/75] Add "Sign in with Google" button This flow is not a great user experience. But it's the fastest way I could think of to add the button that was requested by Google. --- app/k9mail/src/main/AndroidManifest.xml | 4 + .../k9/activity/setup/AccountSetupBasics.kt | 63 +++++++++---- .../fsck/k9/activity/setup/AuthViewModel.kt | 5 + .../k9/activity/setup/OAuthFlowActivity.kt | 89 ++++++++++++++++++ .../btn_google_signin_dark_disabled.9.png | Bin 0 -> 464 bytes .../btn_google_signin_dark_focus.9.png | Bin 0 -> 1289 bytes .../btn_google_signin_dark_normal.9.png | Bin 0 -> 1225 bytes .../btn_google_signin_dark_pressed.9.png | Bin 0 -> 1231 bytes .../btn_google_signin_dark_disabled.9.png | Bin 0 -> 331 bytes .../btn_google_signin_dark_focus.9.png | Bin 0 -> 811 bytes .../btn_google_signin_dark_normal.9.png | Bin 0 -> 758 bytes .../btn_google_signin_dark_pressed.9.png | Bin 0 -> 768 bytes .../btn_google_signin_dark_disabled.9.png | Bin 0 -> 622 bytes .../btn_google_signin_dark_focus.9.png | Bin 0 -> 1615 bytes .../btn_google_signin_dark_normal.9.png | Bin 0 -> 1569 bytes .../btn_google_signin_dark_pressed.9.png | Bin 0 -> 1576 bytes .../btn_google_signin_dark_disabled.9.png | Bin 0 -> 949 bytes .../btn_google_signin_dark_focus.9.png | Bin 0 -> 2783 bytes .../btn_google_signin_dark_normal.9.png | Bin 0 -> 2536 bytes .../btn_google_signin_dark_pressed.9.png | Bin 0 -> 2554 bytes .../res/drawable/btn_google_signin_dark.xml | 7 ++ .../main/res/layout/account_setup_oauth.xml | 64 +++++++++++++ app/ui/legacy/src/main/res/values/strings.xml | 6 ++ 23 files changed, 220 insertions(+), 18 deletions(-) create mode 100644 app/ui/legacy/src/main/java/com/fsck/k9/activity/setup/OAuthFlowActivity.kt create mode 100644 app/ui/legacy/src/main/res/drawable-hdpi/btn_google_signin_dark_disabled.9.png create mode 100644 app/ui/legacy/src/main/res/drawable-hdpi/btn_google_signin_dark_focus.9.png create mode 100644 app/ui/legacy/src/main/res/drawable-hdpi/btn_google_signin_dark_normal.9.png create mode 100644 app/ui/legacy/src/main/res/drawable-hdpi/btn_google_signin_dark_pressed.9.png create mode 100644 app/ui/legacy/src/main/res/drawable-mdpi/btn_google_signin_dark_disabled.9.png create mode 100644 app/ui/legacy/src/main/res/drawable-mdpi/btn_google_signin_dark_focus.9.png create mode 100644 app/ui/legacy/src/main/res/drawable-mdpi/btn_google_signin_dark_normal.9.png create mode 100644 app/ui/legacy/src/main/res/drawable-mdpi/btn_google_signin_dark_pressed.9.png create mode 100644 app/ui/legacy/src/main/res/drawable-xhdpi/btn_google_signin_dark_disabled.9.png create mode 100644 app/ui/legacy/src/main/res/drawable-xhdpi/btn_google_signin_dark_focus.9.png create mode 100644 app/ui/legacy/src/main/res/drawable-xhdpi/btn_google_signin_dark_normal.9.png create mode 100644 app/ui/legacy/src/main/res/drawable-xhdpi/btn_google_signin_dark_pressed.9.png create mode 100644 app/ui/legacy/src/main/res/drawable-xxhdpi/btn_google_signin_dark_disabled.9.png create mode 100644 app/ui/legacy/src/main/res/drawable-xxhdpi/btn_google_signin_dark_focus.9.png create mode 100644 app/ui/legacy/src/main/res/drawable-xxhdpi/btn_google_signin_dark_normal.9.png create mode 100644 app/ui/legacy/src/main/res/drawable-xxhdpi/btn_google_signin_dark_pressed.9.png create mode 100644 app/ui/legacy/src/main/res/drawable/btn_google_signin_dark.xml create mode 100644 app/ui/legacy/src/main/res/layout/account_setup_oauth.xml diff --git a/app/k9mail/src/main/AndroidManifest.xml b/app/k9mail/src/main/AndroidManifest.xml index fa7a23c6a3..08f77966b4 100644 --- a/app/k9mail/src/main/AndroidManifest.xml +++ b/app/k9mail/src/main/AndroidManifest.xml @@ -302,6 +302,10 @@ + + handleCheckSettingsResult(resultCode) + REQUEST_CODE_OAUTH -> handleSignInResult(resultCode) + else -> super.onActivityResult(requestCode, resultCode, data) } + } - if (resultCode == RESULT_OK) { - val account = this.account ?: error("Account instance missing") + private fun handleCheckSettingsResult(resultCode: Int) { + if (resultCode != RESULT_OK) return - if (!checkedIncoming) { - // We've successfully checked incoming. Now check outgoing. - checkedIncoming = true - AccountSetupCheckSettings.actionCheckSettings(this, account, CheckDirection.OUTGOING) - } else { - // We've successfully checked outgoing as well. - preferences.saveAccount(account) - Core.setServicesEnabled(applicationContext) + val account = this.account ?: error("Account instance missing") - AccountSetupNames.actionSetNames(this, account) - } + if (!checkedIncoming) { + // We've successfully checked incoming. Now check outgoing. + checkedIncoming = true + AccountSetupCheckSettings.actionCheckSettings(this, account, CheckDirection.OUTGOING) + } else { + // We've successfully checked outgoing as well. + preferences.saveAccount(account) + Core.setServicesEnabled(applicationContext) + + AccountSetupNames.actionSetNames(this, account) } } + private fun handleSignInResult(resultCode: Int) { + if (resultCode != RESULT_OK) return + + val account = this.account ?: error("Account instance missing") + + AccountSetupCheckSettings.actionCheckSettings(this, account, CheckDirection.INCOMING) + } + private enum class UiState { EMAIL_ADDRESS_ONLY, PASSWORD_FLOW @@ -340,6 +365,8 @@ class AccountSetupBasics : K9Activity() { private const val EXTRA_ACCOUNT = "com.fsck.k9.AccountSetupBasics.account" private const val STATE_KEY_UI_STATE = "com.fsck.k9.AccountSetupBasics.uiState" private const val STATE_KEY_CHECKED_INCOMING = "com.fsck.k9.AccountSetupBasics.checkedIncoming" + private const val REQUEST_CODE_CHECK_SETTINGS = AccountSetupCheckSettings.ACTIVITY_REQUEST_CODE + private const val REQUEST_CODE_OAUTH = Activity.RESULT_FIRST_USER + 1 @JvmStatic fun actionNewAccount(context: Context) { diff --git a/app/ui/legacy/src/main/java/com/fsck/k9/activity/setup/AuthViewModel.kt b/app/ui/legacy/src/main/java/com/fsck/k9/activity/setup/AuthViewModel.kt index 44e101e15b..3d06dd8fb8 100644 --- a/app/ui/legacy/src/main/java/com/fsck/k9/activity/setup/AuthViewModel.kt +++ b/app/ui/legacy/src/main/java/com/fsck/k9/activity/setup/AuthViewModel.kt @@ -69,6 +69,11 @@ class AuthViewModel( return authState.isAuthorized } + fun isUsingGoogle(account: Account): Boolean { + val config = findOAuthConfiguration(account) + return config?.authorizationEndpoint == "https://accounts.google.com/o/oauth2/v2/auth" + } + private fun getOrCreateAuthState(account: Account): AuthState { return try { account.oAuthState?.let { AuthState.jsonDeserialize(it) } ?: AuthState() diff --git a/app/ui/legacy/src/main/java/com/fsck/k9/activity/setup/OAuthFlowActivity.kt b/app/ui/legacy/src/main/java/com/fsck/k9/activity/setup/OAuthFlowActivity.kt new file mode 100644 index 0000000000..fb1d1f07a4 --- /dev/null +++ b/app/ui/legacy/src/main/java/com/fsck/k9/activity/setup/OAuthFlowActivity.kt @@ -0,0 +1,89 @@ +package com.fsck.k9.activity.setup + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import android.view.View +import android.widget.TextView +import androidx.core.view.isVisible +import com.fsck.k9.Account +import com.fsck.k9.preferences.AccountManager +import com.fsck.k9.ui.R +import com.fsck.k9.ui.base.K9Activity +import com.fsck.k9.ui.observe +import org.koin.android.ext.android.inject +import org.koin.androidx.viewmodel.ext.android.viewModel + +class OAuthFlowActivity : K9Activity() { + private val authViewModel: AuthViewModel by viewModel() + private val accountManager: AccountManager by inject() + + private lateinit var errorText: TextView + + public override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setLayout(R.layout.account_setup_oauth) + setTitle(R.string.account_setup_basics_title) + + val accountUUid = intent.getStringExtra(EXTRA_ACCOUNT_UUID) ?: error("Missing account UUID") + val account = accountManager.getAccount(accountUUid) ?: error("Account not found") + + errorText = findViewById(R.id.error_text) + val signInButton: View = if (authViewModel.isUsingGoogle(account)) { + findViewById(R.id.google_sign_in_button) + } else { + findViewById(R.id.oauth_sign_in_button) + } + + signInButton.isVisible = true + signInButton.setOnClickListener { startOAuthFlow(account) } + + authViewModel.init(activityResultRegistry, lifecycle) + + authViewModel.uiState.observe(this) { state -> + handleUiUpdates(state) + } + } + + private fun handleUiUpdates(state: AuthFlowState) { + when (state) { + AuthFlowState.Idle -> { + return + } + AuthFlowState.Success -> { + setResult(RESULT_OK) + finish() + } + AuthFlowState.Canceled -> { + errorText.text = getString(R.string.account_setup_failed_dlg_oauth_flow_canceled) + } + is AuthFlowState.Failed -> { + errorText.text = getString(R.string.account_setup_failed_dlg_oauth_flow_failed, state) + } + AuthFlowState.NotSupported -> { + errorText.text = getString(R.string.account_setup_failed_dlg_oauth_not_supported) + } + AuthFlowState.BrowserNotFound -> { + errorText.text = getString(R.string.account_setup_failed_dlg_browser_not_found) + } + } + + authViewModel.authResultConsumed() + } + + private fun startOAuthFlow(account: Account) { + errorText.text = "" + + authViewModel.login(account) + } + + companion object { + private const val EXTRA_ACCOUNT_UUID = "accountUuid" + + fun buildLaunchIntent(context: Context, accountUuid: String): Intent { + return Intent(context, OAuthFlowActivity::class.java).apply { + putExtra(EXTRA_ACCOUNT_UUID, accountUuid) + } + } + } +} diff --git a/app/ui/legacy/src/main/res/drawable-hdpi/btn_google_signin_dark_disabled.9.png b/app/ui/legacy/src/main/res/drawable-hdpi/btn_google_signin_dark_disabled.9.png new file mode 100644 index 0000000000000000000000000000000000000000..082201ea687e1ddc8fdf5bd7b549432872046be0 GIT binary patch literal 464 zcmV;>0WbcEP)s}3Y0;r}8LiAY2u5|J`di*^jYy90H|(vH-GRoue} zKVl0lT9GQT3**CwsM3ejjVBmCengKhqzRaSp4Noa4ddn;9KHKS?3vK?!>g1(IXqz= zmA_-56`J5Jh=buXCZwis&X|!e8caN z7r)0XYD%@WK98ywM|Ef_h^|oJ^_W~-in2COSJ~=BxfdXL#Dd1=Vpd?~qaW9D6vbLw zz$+YJ9~XFb+uv_jqi(Ql*K~@(G%{?O^m3wI@6_a)+tlO}_Rxn+TPBb@(SRII^ZPK7 zh(shJ5s7Pvh{XRAS4|Sh!bQ~ zfbswU06BT$E?%rYfa^Ve=sI=RG-|*dJ#iyUi#d7UHEhQ=dGb1Q)j@;tJ9pkQX1+Ui z-6~nAHgL{9f9O4Y;xTB#CsC9nOphc?jXHbfKY{KqV6rY=t}kS_C{mX;Z_hu0?HxaH zG-|^hL3b@(tv`Y8E?=-de&{}b={a-KIdjw_N`@jzh4{w8`@9AI_xJtbk?Tx9v);a` z$N7b@_x|zBKCD~*{{C&D_UcABuiU$!!uje+JU6LU{`~Y&kMv54^!@3sLWT7B!Mygi zu|>r+PcsiXYoA0wS%So+{r>*e?c80tc1y8lzvIck+fb`}h0!&g#{n)~})0u*c`m^ZNDpz`OUQ4eV1c^{S*as8jvyyZN*T?`J0LQ$GIr z_*%Gg`nv`5myq!I^1#;ncg2kU_~kp4t?l#uR+9Bjjr3lZ_4>R8+S`_!)T%b3YybcN z&{&uA0000lbW%=JO(p4&SX6S7I00K!#L_t(|0qxlXpBq6KfbmW4V)Ja(J=-_66{ogs`)b>s)OPo) zHS4W4?^gT#IE(qmn5DF&qiOoH+bOM0qeQl(q@k2Z^E)IRR3Aw?zuN$l+pm*LI%h8j zg2~}?>SU6oC#r4vscOG%`7Dy8D{#~XezI>>OlW0at1(i}6iT)=gttzR~e=k+69W!>s#!aJvrG~BJn&ND&yZS>);YeIoGzKPJ zZ;gM@fgwQB`1r>?gpme{K-KU;A_O3ftm2qa1LiaYD*8`kAP{NZeAYR?uz}wY3xKCHa__MN-;;HA3qWDi#GqZ(O{2}@n&ObLF3WKAF} zRkG64&qyy6lU^$J=)Hn?MZA8)j~cl;nbRhbew9xyFP}1X+I0E088eyFW)YSGv&*;4 z#dy1X(*ne_leDyG@e+)2+cIg(@($*#m8(cg4XZKJn(79A!#Wm1>&bs>Y^cX<_c!lv z2t3;MWXCh+q|WE$rNC}1RNsx6_UuJyAIP`+Tl`V)4juyf#d7$_N7T_{KvLlN3AuCf z6yz_<(`VW^=IpuV3&6jYFJ5ZCe5Ly8HTA!SDyh=Hv2LWUP-*{NsYe;$Dt1{9Nz&)_ z23(o%lc$%Yv~+JFf~nA(o+e2;yQgnSkqM%rl71e$PLwFWwKP8?(@eh1jQmoopOU0( zzuj7Fgs9kR_h*X|lTHtnj$+a%ctujVtE}$<8xaP`O^TBX00000NkvXXu0mjf&uGR$ literal 0 HcmV?d00001 diff --git a/app/ui/legacy/src/main/res/drawable-hdpi/btn_google_signin_dark_normal.9.png b/app/ui/legacy/src/main/res/drawable-hdpi/btn_google_signin_dark_normal.9.png new file mode 100644 index 0000000000000000000000000000000000000000..4a5ddfb762a13bd5f52b970697e7d26ddfd9b26f GIT binary patch literal 1225 zcmV;)1UCDLP)W*(000D#Nklms^-RbP$o~o}f8NKth`AAU0 z!EHDk4u_*>9%b}KeI64$DR^4&jNn-&=sctJ`y}-pu%I_{Hhv*{kGK)J`WSEI?q>p{ z57cjt?ze_h-+_vHDtq^vBk7m)TPa~?tSv)AV@oTtvUj^Vf_e{R9rS#z-l?}}5E(sb zMHKZ-y;J}Gu8UryAEc=*qldCy^u`Ts#&oSo3 zNNQA_Bm_iKlbW!fJf^oKmSl|%qq&1hS4YABU|F+VFG(!PGmXw=oOZf5C+rmw}p`3?-i8)A7!)pp@$_okJf}E z{7FdjUPMBfpKePsFP@7u5SAo6O>jiM43b3H);@#e244wT*&*AkNDE;}se4JzJ=nv9 ztR4N~D~op60wVgjgC(hr>(+8NNd&E@d?nC1@XtiG9c!jT#Z1pY`5Lxz+z4~ zSdx~QZmz+~wVxr&!jiWI5q~c#kC>}_;BPL(xln3^`=Ig3}&Slj+&v{b^Ay@x|+XGs@#||Yd zDOj0ubTO5!iN9tyMUKIzF=mq$>cf(JChYPyWLZ*4lc?TX=fUN^9%D(t%<;stsJ5wn zz;cSLs*k8EyFz)Yf-jDYzPbyoV*AtPxHmE4vvQ~%))_0#hQZ--z(-h;o{=0@?2kJJ z5;5!e3d}mO60~W)RCFvQK^lr{x$?^Qi>Ci!aQ}6ytdR*}4KP?Z;=mdZ+&VeZu~8f)5412(k!r2yzA;hI-Aa^!y?9 z9>`l@o)Ekwcwg|9;5)(hL5KNH>Gyr5?;L?=5RQ;jSl%AV*;P>CB`%n00000NkvXXu0mjfL_AhA literal 0 HcmV?d00001 diff --git a/app/ui/legacy/src/main/res/drawable-hdpi/btn_google_signin_dark_pressed.9.png b/app/ui/legacy/src/main/res/drawable-hdpi/btn_google_signin_dark_pressed.9.png new file mode 100644 index 0000000000000000000000000000000000000000..82d16422c70ce338ca08e1ff1ead75b3fd519c06 GIT binary patch literal 1231 zcmV;=1Tg!FP)W*(000D*Nkl~78Vv(ST~0ZtZ=f3vdFS1vZ%=AMN#J)k>{-&C}H+)G4iy#K0AM>hoAFzeniMU z@qF{Ny*Z2Ltrsd%p0?NK@Qn;y{EFsmdlxF>=jJO|dD>i`&2!gt4vLepR zLUq0Z#q;L5^ZcDX7scaGY2GJhvl1X)rbc?Bt{ zI)AH0Hjt75$ty_7Y#8kEhI%zNE|&t=0QEkRH+D=$``a<)#79x*qy|&LqPH*l5+hj znL0LO1buaGAwTzuyODq-4({Q-LpP9C*@|jfp2zE$gIQ?B>z8wDizT#tGyhMzQ5I!emN15e%Kh3 zMEzVEl32UkCxC!teosH2ip@xtemW3^{5xkNQ@xU;eEH%FKtKw3f9g*lt)Guaq`)&H z9e&>GQvtd-}>Z(Uan^ttN1+S=<~`=4AE7GWkEpt)+ZnQWU@^p|1XbRiqv3E zu&*NlspbryN5bK*`2rxh_QFAS_ufp0pVxI|klm_H4Yoq+z6$3gLzmz`os6Xc`5lW& zeRdV6%Ddas7xP!$xA*?DA61!RjWl6rAdn1w$KggJ3(%t@@u~K(2z(Jlq|+?hhm*1Nib{GwnH$j*4%o!#G-nw>F}KPfk01J9l6BQ=OUCdr_sy!SwNq zACQdxn7r-LAq-B{(=Ae2J4MypKSthlXn|C)B#ED(Z^?0a(gp|v03ATeQ3|4 zWAx#fR z*0&8L7k1^%ZEAX`pKhG0yF1YSCnxcv3QzuWhK`TIn5+Y z4sH`jZtau#Go5{T#R9ST<|bMq<4f~n&(70((?|Wc38YXFkbndvAOQ(TKmrnwfP|N= z0TPga1SFi3fD{HuKyqh5N}8|3T{B*C+H>)|dG5|4g_kAF-ssR8ym-6k%HH_kTAn-4 z-`Up%j~g+1^4!_#@7b8Q{T;=94-WBsvo_ehiRaC8=lMJP3j5JyDP?(N#bu>sWsu9` zxt5USQRf{^&wqi5Ad4kSF3Tj#Cd=;fP&ReG$<=vB(EC4=BB-*l)!i_j%R_P1`SP42 tSnWkRu)@j0yDbz>!@bFtcZra@mKh9Zrjkgah{!WxoMx_8{l#mz_DiNKe zs1!t}%$pjSH)S%IjLG~5nO^2Or78b<&rx2)-)fI>C@1(PjMey$6Q%XV91}$J??|e zm@6)UdEYV{8DthEi)kxJq#K;{Ux9m*?hJLON@OAwOWbs(hRMW`)FIYa@gZ($#WxP z0Sku0fCGlY@L^M;FcgNuP#6l+o|^^b?#+HH~Rsw;&7)1H+M(GXWI3|3@IXY(J15!FHD>%b~689xMyO)DYaupHvC z{{7dO8DpVRD)dhJ4imHVPkM)Y1qv?)?h&E{5GG!`;DD zRnnsplRX_BZ{A1C)ej*aqKlT7PXK_zbj0Wrn69+F_!?@4Vg1auhpk!@p3}iYn2^u< zU^)_P|4^AjFIp+guVv2@KElN4^}#fReG5j#-jv22Z_>!fBTX6$Iup{yKbW@6DP1U8 zFFu3MEjK(V4#8k^KcQV?L9fy~_y^M>3c~#-Kf>aMAB=7cf!l*AsA#AK*T|dH7;~)u z8spd*@-<$b;LQ%%L{R$T`EVM-s}fj0mn$vCgmc9ji`n+Dg}-OMbxmT}-yKI#G!pP+ z4pv0%#p>w&81r&z*Vl}%jfVC9u?`cG>T*Y4)GU=i`xZIz_!r_YN#mh)8VT5tF zLrr5nrU$PZ6lLhsdB~9#3Ga@oEJ*=2#T~`CSIgmWZ#*n-kA%*{j64( p`U@eXYd;D%3=M(d9{RGJp8-;_g`eV7@rD2Z002ovPDHLkV1mVvh))0j literal 0 HcmV?d00001 diff --git a/app/ui/legacy/src/main/res/drawable-mdpi/btn_google_signin_dark_normal.9.png b/app/ui/legacy/src/main/res/drawable-mdpi/btn_google_signin_dark_normal.9.png new file mode 100644 index 0000000000000000000000000000000000000000..6b601919ded34b833741aa172e5e343e43991bc9 GIT binary patch literal 758 zcmVz?#(Dn^jFUz|+p!r` z)l1=Hr6SNYttl=W3CE>#%7YQ!zk{x4dweXFCv3IdT0?mIS{X3TJ2#=KRqksja&>(= z@ed=YK0sBiWWey6X0>1#IRN_M{h)4ALtd_J-vn->fqxiXpAJ4C>iU&nKYJ4Xv7bB& z{lM-r3fC|m?J1b2B*(|Hxgep`tpMT8YkE}@?zV&La8%gV#Dlt4HrNjAh8Tizoy);s zq&oJefJYGEpRqgaVSDnPi$j=WiDekqrD8uc%*_3Ef(h2iyLp5;mtBT2ok#`2Ex=h4b3q4hy7+^!sg7XV?sO=) zzB+QRuZ8m4TjQ1KJwioq@u2bCFZY;IV%ak8U|2dG8_7`QR@%`PC*NFxf9`9aqWRLN zJ{L1~hmGHNz8@@Vl-SbL2Zy!h?#}3cW(o$KorcB-`bf+2tZjrM(@sbiQf0Ck@AYne zVOVae9-!>DmNLrMT4}{9Tzw`e<7M#by_qPi%h9oA2|iNu}9Y ozJe2#2`Z#frI{%dNd3Wl0{r{F6R6X$W&i*H07*qoM6N<$f~dZ8?f?J) literal 0 HcmV?d00001 diff --git a/app/ui/legacy/src/main/res/drawable-mdpi/btn_google_signin_dark_pressed.9.png b/app/ui/legacy/src/main/res/drawable-mdpi/btn_google_signin_dark_pressed.9.png new file mode 100644 index 0000000000000000000000000000000000000000..892674099cf6267f937baed8a8d3603153e0362a GIT binary patch literal 768 zcmV+b1ONPqP)&6m`K4}86$uv< zp+)PD)qg^&3aKWOR3$xs5vB72hZ#Lr2c`B}8wxpu^xAo${q!&4RPVVeFwM+04Vjsl ze^tmb{VE*NGwEFyOl>mY--ROPm##UacU>@bWDHYB6$2DT4F(iO8w}l)C=7+6Fcd}t zFua#L*wcr=tX%|V>PRpX2ZLES73`@);OwmA2g65%gWa+U%&0!PvvC>tuuyrx@a`^P zW{mj~iWqkLTIs{^QITN&AE6GgXd2i5P_v#x=n=MBNl>;~?`Yw~vr zQ|#Q8z$j8|)(Sk!stt4OZH~^EdS^)wrqs5n2vg)XNaHXgPv`5*1%n(4qba99LSAA+O+}8b85pZx}B$#xi|8ZU~ZX75GL1mu>vK;yczy~Gaj#ljlVd~bBAmt7(MrF z>A^6u`p`4(h=H%Bq4%qq7^}AeA;}S1hfhe&#Dbf7BFd=Kd5BMu=n39htHT6E%mlqv zFpaQN8_qM{A4~2=DXlHX-sDL4%r`PM2E*SiCMZgoYPbPA?9O7h{dvqc-J$qfpBD=; z3nATw^b^uw y;_3If*A|N2zffd=8bnYgsqtkLT3;&qv7C>V5|5vICx1f#0000x{eyfM7??~wT^vIy7~jslJpEae3~Tn+ z&lT?$`6<&=L!FLK211aW32g1vhnA64IyPiq0iYz_Dq$KH_TUcOHz5J-sza60%Rbe zq)A@N!D}|2e_G9VGhv#TQEvCj#HrHnYIdY{Tg^`Lig>QqkgK(}Q}UUf$abDvf|E}P zl?z5}QdFCI*CT4(3KK=OM^}qHclJhS-uV6?eWiMTNZe$1-Qbv-e@~l&r1|eB&2N9r zB02BpCCSIrxVzTwS$Ey$y4P9T`xdTY`;u4L_FqjB<+R)IM(Fa2O_$h%c9vyChADm$ z=3V!CX+V;l;mZq~UR(R8GNgKnY^=Eeq8=e{- zo#NSJAL_Y(s#H>!rr)o_+wGUlc(Y{xy7HRmFBe=sy1@I)>L>LNL|dX(UEFwPVyn`@ zq!mlUWKa9t(r{rj?+@8AZN>J$w*`^`abC|ag!)cQTe|4gqj_ELc(%n9_eZR{wI}h) zcdHf8j|BUj+7c#NcTmMNLZ08t2d!3oJM$OB#nsR9k3GB2B4sk`t`ouXd*WMmdKI;Vst E0G@gx4FCWD literal 0 HcmV?d00001 diff --git a/app/ui/legacy/src/main/res/drawable-xhdpi/btn_google_signin_dark_focus.9.png b/app/ui/legacy/src/main/res/drawable-xhdpi/btn_google_signin_dark_focus.9.png new file mode 100644 index 0000000000000000000000000000000000000000..bd52cae7c60677c460aa10e39223bf61aeb4af09 GIT binary patch literal 1615 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 literal 0 HcmV?d00001 diff --git a/app/ui/legacy/src/main/res/drawable-xxhdpi/btn_google_signin_dark_disabled.9.png b/app/ui/legacy/src/main/res/drawable-xxhdpi/btn_google_signin_dark_disabled.9.png new file mode 100644 index 0000000000000000000000000000000000000000..545fb14327619f5927b32732a3d36935a950cf41 GIT binary patch literal 949 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 nM{sD40UWw`pDG)^cL{7TWvoL+UHS?yiHfw^J_J literal 0 HcmV?d00001 diff --git a/app/ui/legacy/src/main/res/drawable-xxhdpi/btn_google_signin_dark_normal.9.png b/app/ui/legacy/src/main/res/drawable-xxhdpi/btn_google_signin_dark_normal.9.png new file mode 100644 index 0000000000000000000000000000000000000000..3fb4c67209d81d6656330093789f4d95dac5850e GIT binary patch literal 2536 zcmZWr2{>D67fytUMEOaXmZFJ9i7HiFtF@G<#B?mNt7U3$6s;v_OQ~8yk%o?)F@oBQ zMks1pTkT6NCABrGRim*lsqo*@nJU3Fgo7W`7pnq#mj4r zp<4cCQS}p3(Y=`mhgTmc)f5bTO|S@E7-@Dsi|2oooF=Sq$&Y#X@a`a@nY3f&Vr<0jtlA>Wn zvy}RJeFIiIvCy)wGIOpeOqyY(S}{Q4L?Ah?+|3sGGiTP$2wXBK*@a$~-m@s6>uzfn z1#-sHv{OaQ&yXJ33UCD%o4hVH$RfSuo%v0 z&J4P@Y;5dh;-jgx&50zU6s3M26TW!-i?Qcu`I0Jo-EEjdzi;5e+JwfuHfihUm1dWU z+tgMLG?}KD=*sN%H~!~PqVURHTlFPQZ}Ix(S94N@E(k%gz9JQZL=x<-jNr8yR{Wvd+5rT|rFRSRM}ymj|Qem1aN@U_t~6Mi~}Gg7Fd9 z2nd=7jTD*r0jDRU$AbVPC{4h-IZ^}(CWBx&UL2sL$W!S*m{?bk)$ofPVk`tf(LX|6o|RVrllX~izgayN z741?|?qNVQ)aBP;SecovAjwR^so>e|BehCEAQ;~*QT5)Jeyaz#_eJVs_py;Qr!7~x zBsHi^b2TgWDj$N6n&h>CtVI!Y(}IBfv@55>WPo|6tEDc?U*IiVHx9{JLrfIqW9xj8 zbM&5`GD4`nkO(DWUTpkLC&Ic?FLMQ3SE>vv`!#NTE76qarcQm<-ZnOfD1wLsGFycW z^2;LX4QVqISl#U-4l0BJ`b8PBSrC3o1}FC0)9!Zk^#dim>j)jWX+BsJ2V~u!d04i| zqeS#XyqAOaZbwwr`=(%{_a6tIB4xw?JapY4=dJjJTJ8CD%tTLWAc7sTKcRs5*LM!c z0j4LbPxr{qS?%u`#*Hj88@FeG3eP9KEf#JrarOR@Ok6E9HBeY}zBS!)g8cvhLgi9L zAa*+O*cv3%YMy;ukH9{|jt2Dot-8Cy)akYc=ai>42kamLrYocC!Z$y1TiHnqZ|GG6 zCq*{H^A2II?eYNX;n?CJc+ZisYSomsjosExp`+g%KOeHbq+&Tf;X?h@GFOJ7KMqi??j9=(;6( z$bvBZe8ba{9jFQC%<$L5p8-G*OCn1kP$al({`j&|7NET1VFWime`6;MC#9j54MQUf z&r9B8WDb-_QF;%cHATm3BA2K@P1c=6j3;yKJZ=mqo7Wx1`*1TgA&NV$!0i5+v`Rz)g27tkOycQ?c4Lcru*HNKdp|W5%u*b)@ z4PWrj$tTYyVG>tNXDMw&)FAM{_ukA%#V#l*cu-iap9&?jTi;hx>Lz{x` z)X zpidrhUyE)pdO1R5-o^VL)#C-$r2@^(PSs!f3}riBHx$bVa(`T}?W^Jk8IuxZfzNMgb>FlOO`p~*mG%VS#Mu9MgD4ex||*lK{%<0 z-Ffj*-}YA5${W?fiRo}c*c~HGL-qBwPBp8b)~4x7-{JjPGw~4r_}=E{mz82?MAMkE zMMB>j|Nl38UNrnK>iC-|{^5W)HUOdT&aAYM7I|d< zK!T#=zq$qkhuH$Sxke}IseHiPu@KGl#?Io2YcWptJ6jT3M_nE{_BYIDGUMQm0bS=C z70+U!f-_F}(Y}{$e@TFtoJ)8z2!r0J{)2z&nc2=GctxOUo2Y`E*Pf0*9>>-kh zkrjrB0wGASf(Xd8Y)M26BkU0nFrhDP|7gGGdGC3jd+#~F_nz~fdwz%NXm70`uPQGg zA)#P{Mqz+=4=`x5I{+CWclxM=gxqTzl%+EnG&>R$44+bsUVI3de((}EpNJc-GWQ>b z3|HYEj?N4S1iR%U#Fvo%u%m07n$d&ctP~-JTso)q1GY--fGN z?cizqVa0kC2X_Wj_Z6IDkg)R$y144q9t4g!yk8mR;HoA@iS3irLJu1_t++M66FT%C zS|a~;@?7QxFZ|vc;|&bb$C)tHZ)N9AJpEpdCh+#SF>dGV%DRo&4X%b;Sa2isr#-MYb~a8t$tk`q4r{Qju$9g=>|x`H7Qgf84+5ExnsZQ)9C}aY zBhtDW2GaYcS`rRT;)b|DqO4lIEyB$Mb&--vQy#_$dJ~VfR0Mgo9}1$blrfh;)M(}F z=+FD$l1embw6r>O-5jb&lSLz8fI_r1BUK9i?>T&X6$M}cUrSXv{4dRa???lamT6&} zu_?wZC_i5K=AgGw0G%TyG>#M&*D%xelK6R_%RKBq+7-vMtq~RVVzgPxTZ*xfDjF%7 zOxiq95&7-x#}Hi#`|RWUz0b=oaUykdLzs>Jm#cRG&^G$)pN3EY)x~tkZn|UWf%o0m zn)f^9(8#6Zx#bxi`Vm+C`_X1?2YmIm;nuo90c(Ec8D@o-3Biiyt04RKHb5EB)YSCl%Q zvJz?{qXcSE?bATj1FNB~#}_qX1V@`jO44iCRZ66cbio7vIJTS3G1&7x)aWk>@rtBU z1B=n_t7c=?aiq15U=aDpn&#Fi*ZIT_>r_<$E3DH09_TWGba1}UP9?MvtZZ-v2>}qJaXR zw5@SW!=|6B5-mo9Rz?UPGGZ{sg5E$dohP@XF|v|9wVS_K{b*Ksbms6&6?w*9X@(ke zdEj2^tx@EFm7nP+7!3%T7jw;jz)DhyFl!WR&Uh>ORXMq4aKSdecCafP2ZnbA>?4}J6n6DPF;E)?cmD-vY>WC=Vn`YH#aP1>g%=opedG`1J{>6u#N2X|QmMTi*wtCle`%BEOR$l~kejM6PFCr&usU|-MT$qUIJ~t9!9#Y4 z^~gd)e?RBd&uW;N3NrLAHr$7o`CX^Y=uP42-;{+TCpCYbFe2LJ;ZVX&&o5K+3sk~s zhbu`JxH(Q&p4e1+e|Y#VH>KN>rP?bV6!%WXXW0Ok{_cwlYPojjUqlVgVo2xr-_aEL zb<8hqCd5Vyg)?x4Mj~@nQ$qU%Xe#)9|MOejJ{WH|utJk-X-2(nfnLt;ck8yEbg~mg zAP&5E+`)`?R*lx-{?pU9bsamXB2_)XQJD_N>NLw1LwT>TV>rPBH`fPLg2(t#0`qj@ z!?MpgRR+d*R%i2>F$-O<4g_9qCmP-!-94IbypXJN=5TU9UNCft#FkJHIV)Y_mtC z!j0_!r!{d(lRAxr83siUj9)%ZJ!EjXH6p0H&v+@cZV@S89?fw7HtbgUM?E&sxAF@j z{6t25N_bEn_jd^98d1%C3*X)C^&@Luj}c!O6H{emz<&f}vv!G~+9$_zoQD0`8>_wS z^h-8PGn0Nun(-`gOJ&(wEmFZ;b0jq%6WEZfOo_}UY{r)Z=`ndSf)(mGc{nRB;SiKx zZ$n$t9R7L*s4;il&*0Qlj51C24{1;6Eu^@D_t7v_lhwT#yeqRS!Zo|kamS3hKBPQW zOMoM`sRJO4za-$bA}v}u9}xer#DA5Ppi{ucKip!w zZTFW&jmQqDS`et>t!*=m*53--z*mavimjJOiS^$$UjvkS2^Cx5W%Y?+J72>Msa!5Y zBE!D+5)pY(XFkzb$H4VyhL_a>!*-T@pDRRKww8u{%BpqZd=o{Vt3BrPx%EqzZc^lI zT(FG2;E%+przDTGVIB?UY~r$3>`7*|r+Z{A9;8jC8M&`cf{od^PWwj*=s_1!VQ$?l zwYAq1|LUg0_{KT-UL5!K@R$pe365%=2_r^X^`!KEY$A72POM%RDst+2h;4**gtf=$ z-CN$1l|_?VPFOHQ&$+sb)f)UNLm?bV_xYI`a*b)@-nzH*w=+<(SEK z4*DKC~UmPdS_7vmdCH%3Gh3@(;%IHfz4^MBbuo}U$se{#rNOLIVI zzAxwWfZkdr;S|RevF*`VOqNZDtPVn-P>>uk@W%N}IYNTPIeh zKs!SU)_Sw`aVwrU$Vb3 + + + + + + diff --git a/app/ui/legacy/src/main/res/layout/account_setup_oauth.xml b/app/ui/legacy/src/main/res/layout/account_setup_oauth.xml new file mode 100644 index 0000000000..e352d760c2 --- /dev/null +++ b/app/ui/legacy/src/main/res/layout/account_setup_oauth.xml @@ -0,0 +1,64 @@ + + + + + + + + + + + +