Repo created

This commit is contained in:
Fr4nz D13trich 2025-11-22 13:56:56 +01:00
parent 75dc487a7a
commit 39c29d175b
6317 changed files with 388324 additions and 2 deletions

View file

@ -0,0 +1,18 @@
plugins {
id(ThunderbirdPlugins.Library.jvm)
alias(libs.plugins.android.lint)
}
val testCoverageEnabled: Boolean by extra
if (testCoverageEnabled) {
apply(plugin = "jacoco")
}
dependencies {
api(projects.mail.common)
api(projects.core.common)
api(libs.okio)
api(libs.junit)
api(libs.assertk)
}

View file

@ -0,0 +1,5 @@
package com.fsck.k9.mail.testing
fun String.crlf() = replace("\n", "\r\n")
fun String.removeLineBreaks() = replace(Regex("""\r|\n"""), "")

View file

@ -0,0 +1,15 @@
package com.fsck.k9.mail.testing;
import com.fsck.k9.mail.filter.Base64;
public class XOAuth2ChallengeParserTestData {
public static final String STATUS_400_RESPONSE = Base64.encode(
"{\"status\":\"400\",\"schemes\":\"bearer mac\",\"scope\":\"https://mail.google.com/\"}");
public static final String STATUS_401_RESPONSE = Base64.encode(
"{\"status\":\"401\",\"schemes\":\"bearer mac\",\"scope\":\"https://mail.google.com/\"}");
public static final String MISSING_STATUS_RESPONSE = Base64.encode(
"{\"schemes\":\"bearer mac\",\"scope\":\"https://mail.google.com/\"}");
public static final String INVALID_RESPONSE = Base64.encode(
"{\"status\":\"401\",\"schemes\":\"bearer mac\",\"scope\":\"https://mail.google.com/\"");
}

View file

@ -0,0 +1,41 @@
@file:Suppress("TooManyFunctions")
package com.fsck.k9.mail.testing.assertk
import assertk.Assert
import assertk.assertions.prop
import com.fsck.k9.mail.Body
import com.fsck.k9.mail.BodyPart
import com.fsck.k9.mail.Part
import com.fsck.k9.mail.internet.MimeMultipart
import com.fsck.k9.mail.internet.MimeParameterDecoder
import com.fsck.k9.mail.internet.MimeUtility
import com.fsck.k9.mail.internet.MimeValue
import com.fsck.k9.mail.internet.RawDataBody
import com.fsck.k9.mail.internet.TextBody
fun Assert<Part>.body() = prop(Part::getBody)
@JvmName("textBodyEncoding")
fun Assert<TextBody>.contentTransferEncoding() = prop(TextBody::getEncoding)
@JvmName("rawDataBodyEncoding")
fun Assert<RawDataBody>.contentTransferEncoding() = prop(RawDataBody::getEncoding)
fun Assert<Body>.asBytes() = transform { it.inputStream.readBytes() }
fun Assert<Body>.asText() = transform {
String(MimeUtility.decodeBody(it).readBytes())
}
fun Assert<MimeMultipart>.bodyParts() = transform { it.bodyParts }
fun Assert<MimeMultipart>.bodyPart(index: Int): Assert<BodyPart> = transform { it.getBodyPart(index) }
fun Assert<Part>.mimeType() = transform { it.mimeType }
fun Assert<Part>.contentType() = transform { MimeParameterDecoder.decode(it.contentType) }
fun Assert<MimeValue>.value() = transform { it.value }
fun Assert<MimeValue>.parameter(name: String): Assert<String?> = transform { it.parameters[name] }

View file

@ -0,0 +1,68 @@
package com.fsck.k9.mail.testing.message
import com.fsck.k9.mail.Message
import com.fsck.k9.mail.Multipart
import com.fsck.k9.mail.Part
import com.fsck.k9.mail.internet.MimeBodyPart
import com.fsck.k9.mail.internet.MimeMessage
import com.fsck.k9.mail.internet.MimeMessageHelper
import com.fsck.k9.mail.internet.MimeMultipart
import com.fsck.k9.mail.internet.TextBody
import com.fsck.k9.mailstore.BinaryMemoryBody
fun buildMessage(block: PartBuilder.() -> Unit): Message {
return MimeMessage().also { message ->
PartBuilder(message).block()
}
}
@DslMarker
annotation class MessageBuilderMarker
@MessageBuilderMarker
class PartBuilder(private val part: Part) {
private var gotBodyBlock = false
fun header(name: String, value: String) {
part.addHeader(name, value)
}
fun textBody(text: String = "Hello World") {
require(!gotBodyBlock) { "Only one body block allowed" }
gotBodyBlock = true
val body = TextBody(text)
MimeMessageHelper.setBody(part, body)
}
fun dataBody(size: Int = 20 * 1024, encoding: String = "7bit") {
require(!gotBodyBlock) { "Only one body block allowed" }
gotBodyBlock = true
val body = BinaryMemoryBody(ByteArray(size) { 'A'.code.toByte() }, encoding)
MimeMessageHelper.setBody(part, body)
}
fun multipart(subType: String = "mixed", block: MultipartBuilder.() -> Unit) {
require(!gotBodyBlock) { "Only one body block allowed" }
gotBodyBlock = true
val multipart = MimeMultipart.newInstance()
multipart.setSubType(subType)
MultipartBuilder(multipart).block()
MimeMessageHelper.setBody(part, multipart)
}
}
@MessageBuilderMarker
class MultipartBuilder(private val multipart: Multipart) {
fun bodyPart(mimeType: String? = null, block: PartBuilder.() -> Unit) {
MimeBodyPart().let { bodyPart ->
if (mimeType != null) {
bodyPart.addHeader("Content-Type", mimeType)
}
multipart.addBodyPart(bodyPart)
PartBuilder(bodyPart).block()
}
}
}

View file

@ -0,0 +1,77 @@
package com.fsck.k9.mail.testing.message;
import java.io.IOException;
import java.io.OutputStream;
import com.fsck.k9.mail.Address;
import net.thunderbird.core.common.exception.MessagingException;
import com.fsck.k9.mail.internet.MimeMessage;
import okio.BufferedSink;
import okio.Okio;
class TestMessage extends MimeMessage {
private final long messageSize;
private final Address[] from;
private final Address[] to;
private final boolean hasAttachments;
TestMessage(TestMessageBuilder builder) {
from = toAddressArray(builder.from);
to = toAddressArray(builder.to);
hasAttachments = builder.hasAttachments;
messageSize = builder.messageSize;
}
@Override
public Address[] getFrom() {
return from;
}
@Override
public Address[] getRecipients(RecipientType type) {
switch (type) {
case TO:
return to;
case CC:
case BCC:
case X_ORIGINAL_TO:
case DELIVERED_TO:
case X_ENVELOPE_TO:
return new Address[0];
}
throw new AssertionError("Missing switch case: " + type);
}
@Override
public boolean hasAttachments() {
return hasAttachments;
}
@Override
public long calculateSize() {
return messageSize;
}
@Override
public void writeTo(OutputStream out) throws IOException, MessagingException {
BufferedSink bufferedSink = Okio.buffer(Okio.sink(out));
bufferedSink.writeUtf8("[message data]");
bufferedSink.emit();
}
private static Address[] toAddressArray(String[] emails) {
return emails == null ? new Address[0] : stringArrayToAddressArray(emails);
}
private static Address[] stringArrayToAddressArray(String[] emails) {
Address addresses[] = new Address[emails.length];
for (int i = 0; i < emails.length; i++) {
addresses[i] = new Address(emails[i]);
}
return addresses;
}
}

View file

@ -0,0 +1,37 @@
package com.fsck.k9.mail.testing.message;
import com.fsck.k9.mail.Message;
public class TestMessageBuilder {
String[] from;
String[] to;
boolean hasAttachments;
long messageSize;
public TestMessageBuilder from(String... email) {
from = email;
return this;
}
public TestMessageBuilder to(String... email) {
to = email;
return this;
}
public TestMessageBuilder setHasAttachments(boolean hasAttachments) {
this.hasAttachments = hasAttachments;
return this;
}
public TestMessageBuilder messageSize(long messageSize) {
this.messageSize = messageSize;
return this;
}
public Message build() {
return new TestMessage(this);
}
}

View file

@ -0,0 +1,62 @@
package com.fsck.k9.mail.testing.message;
import com.fsck.k9.mail.Body;
import com.fsck.k9.mail.BodyPart;
import net.thunderbird.core.common.exception.MessagingException;
import com.fsck.k9.mail.internet.MimeBodyPart;
import com.fsck.k9.mail.internet.MimeHeader;
import com.fsck.k9.mail.internet.MimeMessage;
import com.fsck.k9.mail.internet.MimeMessageHelper;
import com.fsck.k9.mail.internet.MimeMultipart;
import com.fsck.k9.mail.internet.TextBody;
public class TestMessageConstructionUtils {
public static MimeMessage messageFromBody(String subject, BodyPart bodyPart) throws MessagingException {
MimeMessage message = messageFromBody(bodyPart);
message.setSubject(subject);
return message;
}
public static MimeMessage messageFromBody(BodyPart bodyPart) throws MessagingException {
MimeMessage message = new MimeMessage();
MimeMessageHelper.setBody(message, bodyPart.getBody());
if (bodyPart.getContentType() != null) {
message.setHeader("Content-Type", bodyPart.getContentType());
}
message.setUid("msguid");
return message;
}
public static MimeBodyPart multipart(String type, BodyPart... subParts) throws MessagingException {
return multipart(type, null, subParts);
}
public static MimeBodyPart multipart(String type, String typeParameters, BodyPart... subParts) throws MessagingException {
MimeMultipart multiPart = MimeMultipart.newInstance();
multiPart.setSubType(type);
for (BodyPart subPart : subParts) {
multiPart.addBodyPart(subPart);
}
MimeBodyPart mimeBodyPart = new MimeBodyPart(multiPart);
if (typeParameters != null) {
mimeBodyPart.setHeader(MimeHeader.HEADER_CONTENT_TYPE,
mimeBodyPart.getContentType() + "; " + typeParameters);
}
return mimeBodyPart;
}
public static BodyPart bodypart(String type) throws MessagingException {
return new MimeBodyPart(null, type);
}
public static MimeBodyPart bodypart(String type, String text) throws MessagingException {
TextBody textBody = new TextBody(text);
return new MimeBodyPart(textBody, type);
}
public static BodyPart bodypart(String type, Body body) throws MessagingException {
return new MimeBodyPart(body, type);
}
}

View file

@ -0,0 +1,20 @@
package com.fsck.k9.mail.testing.security
import com.fsck.k9.mail.CertificateChainException
import java.security.cert.X509Certificate
import javax.net.ssl.X509TrustManager
@Suppress("CustomX509TrustManager")
class FakeTrustManager : X509TrustManager {
var shouldThrowException = false
override fun checkClientTrusted(chain: Array<out X509Certificate>?, authType: String?) = Unit
override fun checkServerTrusted(chain: Array<out X509Certificate>?, authType: String?) {
if (shouldThrowException) {
throw CertificateChainException("Test", chain, Exception("cause"))
}
}
override fun getAcceptedIssuers(): Array<X509Certificate> = emptyArray()
}

View file

@ -0,0 +1,41 @@
package com.fsck.k9.mail.testing.security
import com.fsck.k9.mail.ClientCertificateError
import com.fsck.k9.mail.ClientCertificateException
import com.fsck.k9.mail.ssl.TrustedSocketFactory
import java.net.Socket
import javax.net.ssl.SSLContext
import javax.net.ssl.TrustManager
import javax.net.ssl.X509TrustManager
class SimpleTrustedSocketFactory(private val trustManager: X509TrustManager) : TrustedSocketFactory {
private var clientCertificateError: ClientCertificateError? = null
override fun createSocket(socket: Socket?, host: String, port: Int, clientCertificateAlias: String?): Socket {
requireNotNull(socket)
@Suppress("ThrowingExceptionsWithoutMessageOrCause")
when (val error = clientCertificateError) {
ClientCertificateError.RetrievalFailure -> throw ClientCertificateException(error, RuntimeException())
ClientCertificateError.CertificateExpired -> throw ClientCertificateException(error, RuntimeException())
null -> Unit
}
val trustManagers = arrayOf<TrustManager>(trustManager)
val sslContext = SSLContext.getInstance("TLS").apply {
init(null, trustManagers, null)
}
return sslContext.socketFactory.createSocket(
socket,
socket.inetAddress.hostAddress,
socket.port,
true,
)
}
fun injectClientCertificateError(error: ClientCertificateError) {
clientCertificateError = error
}
}

View file

@ -0,0 +1,27 @@
package com.fsck.k9.mail.testing.security
import java.security.KeyStore
import java.security.cert.X509Certificate
object TestKeyStoreProvider {
private const val KEYSTORE_PASSWORD = "password"
private const val KEYSTORE_RESOURCE = "/keystore.jks"
private const val SERVER_CERTIFICATE_ALIAS = "mockimapserver"
val keyStore: KeyStore by lazy { loadKeyStore() }
val password: CharArray by lazy { KEYSTORE_PASSWORD.toCharArray() }
val serverCertificate: X509Certificate by lazy {
keyStore.getCertificate(SERVER_CERTIFICATE_ALIAS) as X509Certificate
}
private fun loadKeyStore(): KeyStore {
val keyStore = KeyStore.getInstance("JKS")
val keyStoreInputStream = TestKeyStoreProvider::class.java.getResourceAsStream(KEYSTORE_RESOURCE)
keyStoreInputStream.use { inputStream ->
keyStore.load(inputStream, KEYSTORE_PASSWORD.toCharArray())
}
return keyStore
}
}

View file

@ -0,0 +1,53 @@
package com.fsck.k9.mail.testing.security
import com.fsck.k9.mail.ssl.TrustedSocketFactory
import java.io.IOException
import java.net.Socket
import java.security.KeyManagementException
import java.security.NoSuchAlgorithmException
import java.security.cert.X509Certificate
import javax.net.ssl.SSLContext
import javax.net.ssl.TrustManager
import net.thunderbird.core.common.exception.MessagingException
/**
* A test trusted socket factory that creates sockets that trust only a predefined server certificate
*/
object TestTrustedSocketFactory : TrustedSocketFactory {
private val serverCertificate: X509Certificate by lazy {
TestKeyStoreProvider.serverCertificate
}
@Throws(
NoSuchAlgorithmException::class,
KeyManagementException::class,
MessagingException::class,
IOException::class,
)
override fun createSocket(
socket: Socket?,
host: String,
port: Int,
clientCertificateAlias: String?,
): Socket {
val trustManagers: Array<TrustManager> = arrayOf(VeryTrustingTrustManager(serverCertificate))
val sslContext = SSLContext.getInstance("TLS").apply {
init(null, trustManagers, null)
}
val sslSocketFactory = sslContext.socketFactory
return if (socket == null) {
sslSocketFactory.createSocket(host, port)
} else {
sslSocketFactory.createSocket(
socket,
socket.inetAddress.hostAddress,
socket.port,
true,
)
}
}
}

View file

@ -0,0 +1,32 @@
package com.fsck.k9.mail.testing.security
import java.security.cert.CertificateException
import java.security.cert.X509Certificate
import javax.net.ssl.X509TrustManager
/**
* A very trusting trust manager that accepts all certificates. It is used in tests to accept all certificates.
*
* WARNING: This trust manager is very insecure and should never be used in production code!
*
* @param serverCertificate The server certificate to return as the accepted issuer.
*/
@Suppress("CustomX509TrustManager")
internal class VeryTrustingTrustManager(private val serverCertificate: X509Certificate?) : X509TrustManager {
/**
* Always trust the client certificate.
*/
@Throws(CertificateException::class)
override fun checkClientTrusted(chain: Array<X509Certificate?>?, authType: String?) = Unit
/**
* Always trust the server certificate.
*/
@Throws(CertificateException::class)
override fun checkServerTrusted(chain: Array<X509Certificate?>?, authType: String?) = Unit
override fun getAcceptedIssuers(): Array<X509Certificate?> {
return arrayOf<X509Certificate?>(serverCertificate)
}
}