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