diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index e0a4f000e0c8137479e7870378a834101d2245d6..5ca09ac83668a610aadbd96169424f2a88ffd205 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -2,7 +2,7 @@ image: "registry.gitlab.e.foundation/e/os/docker-android-apps-cicd:master"
variables:
APK_PATH: "app/k9mail/build/outputs/apk/release"
- UNSIGNED_APK: "Mail-unsigned.apk"
+ UNSIGNED_APK: "k9mail-release-unsigned.apk"
COMMUNITY_APK: "Mail-community.apk"
OFFICIAL_APK: "Mail-official.apk"
TEST_APK: "Mail-test.apk"
@@ -29,19 +29,10 @@ test:
build:
stage: build
script:
- - ./gradlew :app:k9mail:build
- - cd app/k9mail/build/outputs/apk/
- - |
- if [[ ! -d "release" ]]; then
- echo "$APK_PATH does not exist."
- exit 1
- fi
- cd "release"
- unsigned_build=$(ls *.apk | grep "unsigned")
- cp $unsigned_build $UNSIGNED_APK
+ - ./gradlew :app:k9mail:assembleRelease
artifacts:
paths:
- - app/k9mail/build/outputs/apk/
+ - $APK_PATH
init_submodules:
stage: gitlab_release
diff --git a/app/k9mail/src/main/AndroidManifest.xml b/app/k9mail/src/main/AndroidManifest.xml
index 096d89448925e6dbe9f538a3c061e19e0bbc3193..d534fcbcdeefdb0c22e70875bfeb173ca1894f20 100644
--- a/app/k9mail/src/main/AndroidManifest.xml
+++ b/app/k9mail/src/main/AndroidManifest.xml
@@ -36,7 +36,11 @@
android:label="@string/app_name"
android:theme="@style/Theme.K9.Startup"
android:resizeableActivity="true"
- android:allowBackup="false"
+ android:allowBackup="true"
+ android:backupAgent="com.fsck.k9.MailBackupAgent"
+ android:fullBackupContent="@xml/backup_rules"
+ android:dataExtractionRules="@xml/data_extraction_rules"
+ android:fullBackupOnly="true"
android:supportsRtl="true"
android:hasFragileUserData="false"
tools:replace="android:theme"
diff --git a/app/k9mail/src/main/java/com/fsck/k9/MailBackupAgent.kt b/app/k9mail/src/main/java/com/fsck/k9/MailBackupAgent.kt
new file mode 100644
index 0000000000000000000000000000000000000000..4d8e9c7194de1c3721e8a90132cdeaa9228f437f
--- /dev/null
+++ b/app/k9mail/src/main/java/com/fsck/k9/MailBackupAgent.kt
@@ -0,0 +1,90 @@
+/*
+ * Copyright (C) 2026 e Foundation
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ */
+package com.fsck.k9
+
+import android.app.backup.BackupAgent
+import android.app.backup.BackupDataInput
+import android.app.backup.BackupDataOutput
+import android.app.backup.FullBackupDataOutput
+import android.os.Build
+import android.os.ParcelFileDescriptor
+import androidx.annotation.RequiresApi
+import java.io.File
+import java.io.IOException
+import timber.log.Timber
+
+/*
+ * Note:
+ * - We rely only on full backup.
+ * - Backup is allowed only for encrypted or device-to-device transports.
+ * - Only selected app data is backed up (databases + shared preferences).
+ */
+class MailBackupAgent : BackupAgent() {
+ override fun onBackup(
+ oldState: ParcelFileDescriptor?,
+ data: BackupDataOutput?,
+ newState: ParcelFileDescriptor?
+ ) = Unit
+
+ override fun onRestore(
+ data: BackupDataInput?,
+ appVersionCode: Int,
+ newState: ParcelFileDescriptor?
+ ) = Unit
+
+ @RequiresApi(Build.VERSION_CODES.P)
+ override fun onFullBackup(data: FullBackupDataOutput) {
+ val flags = data.transportFlags
+ val allowed = flags and FLAG_CLIENT_SIDE_ENCRYPTION_ENABLED != 0 ||
+ flags and FLAG_DEVICE_TO_DEVICE_TRANSFER != 0
+
+ if (!allowed) return
+
+ BACKUP_DIRS.forEach { backupDir(File(dataDir, it), data) }
+ }
+
+ private fun backupDir(dir: File, data: FullBackupDataOutput) {
+ if (!dir.isDirectory) return
+
+ try {
+ dir.listFiles()?.forEach { file ->
+ if (file.isDirectory) {
+ backupDir(file, data)
+ } else if (shouldBackupFile(file)) {
+ fullBackupFile(file, data)
+ }
+ }
+ } catch (e: SecurityException) {
+ Timber.e(e, "Skipping restricted")
+ } catch (e: IOException) {
+ Timber.e(e, "I/O error during backup")
+ }
+ }
+
+ private fun shouldBackupFile(file: File) =
+ EXCLUDED_SUFFIXES.none(file.name::endsWith)
+
+ companion object {
+ private val EXCLUDED_SUFFIXES = listOf("-journal", "-shm", "-wal")
+
+ private val BACKUP_DIRS = listOf(
+ "databases",
+ "shared_prefs"
+ )
+ }
+}
diff --git a/app/k9mail/src/main/res/xml/backup_rules.xml b/app/k9mail/src/main/res/xml/backup_rules.xml
new file mode 100644
index 0000000000000000000000000000000000000000..187cb6b432254a590744ffde8a02feeb6018e558
--- /dev/null
+++ b/app/k9mail/src/main/res/xml/backup_rules.xml
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
+
diff --git a/app/k9mail/src/main/res/xml/data_extraction_rules.xml b/app/k9mail/src/main/res/xml/data_extraction_rules.xml
new file mode 100644
index 0000000000000000000000000000000000000000..e04cdd137007584d9006ec67e4468d1e8d3ef0d5
--- /dev/null
+++ b/app/k9mail/src/main/res/xml/data_extraction_rules.xml
@@ -0,0 +1,33 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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 140fa61df75134e2a3ca1ac08bdddbbdaa960e08..a547ed0be9bc97c45e5e00a5bc77f19cfa51e5f9 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
@@ -282,8 +282,6 @@ public class MockImapServer {
while (!shouldStop) {
readAdditionalCommands();
}
-
- waitForConnectionClosed.countDown();
} catch (UnexpectedCommandException e) {
unexpectedCommandException = e;
} catch (IOException e) {
@@ -293,10 +291,11 @@ public class MockImapServer {
} catch (KeyStoreException | CertificateException | UnrecoverableKeyException |
NoSuchAlgorithmException | KeyManagementException e) {
throw new RuntimeException(e);
+ } finally {
+ waitForConnectionClosed.countDown();
+ IOUtils.closeQuietly(socket);
}
- IOUtils.closeQuietly(socket);
-
logger.log("Exiting");
}
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 bbcca763081b1d5893450ffbf592409f494b8f36..cc30af1f550563f8ae492decda8007ab78606249 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
@@ -82,12 +82,14 @@ class Pop3Connection {
performAuthentication(settings.getAuthType(), serverGreeting);
} catch (SSLException e) {
+ close();
if (e.getCause() instanceof CertificateException) {
throw new CertificateValidationException(e.getMessage(), e);
} else {
throw new MessagingException("Unable to connect", e);
}
} catch (GeneralSecurityException gse) {
+ close();
throw new MessagingException(
"Unable to open connection to POP server due to security error.", gse);
} catch (IOException ioe) {
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 a5142829db503e851086ef680bc7f3c778a25a77..36b0e48ac4700c6a6c19a0565110aff4df37fdcd 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
@@ -282,8 +282,6 @@ public class MockPop3Server {
while (!shouldStop) {
readAdditionalCommands();
}
-
- waitForConnectionClosed.countDown();
} catch (UnexpectedCommandException e) {
unexpectedCommandException = e;
} catch (IOException e) {
@@ -293,10 +291,11 @@ public class MockPop3Server {
} catch (KeyStoreException | CertificateException | UnrecoverableKeyException |
NoSuchAlgorithmException | KeyManagementException e) {
throw new RuntimeException(e);
+ } finally {
+ waitForConnectionClosed.countDown();
+ IOUtils.closeQuietly(socket);
}
- IOUtils.closeQuietly(socket);
-
logger.log("Exiting");
}
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 35ece7ab7ccc4f2c561a796e3c86782166d84ea5..b8fb14aefc20d66a160b09a973095b6e25f526d7 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
@@ -284,8 +284,6 @@ public class MockSmtpServer {
while (!shouldStop) {
readAdditionalCommands();
}
-
- waitForConnectionClosed.countDown();
} catch (UnexpectedCommandException e) {
unexpectedCommandException = e;
} catch (IOException e) {
@@ -295,10 +293,11 @@ public class MockSmtpServer {
} catch (KeyStoreException | CertificateException | UnrecoverableKeyException |
NoSuchAlgorithmException | KeyManagementException e) {
throw new RuntimeException(e);
+ } finally {
+ waitForConnectionClosed.countDown();
+ IOUtils.closeQuietly(socket);
}
- IOUtils.closeQuietly(socket);
-
logger.log("Exiting");
}