commit
dd17a2bb5e
51 changed files with 3165 additions and 2 deletions
|
@ -25,4 +25,5 @@ val mainModule = module {
|
|||
single<TrustedSocketFactory> { DefaultTrustedSocketFactory(get(), get()) }
|
||||
single { Clock.INSTANCE }
|
||||
factory { ServerNameSuggester() }
|
||||
factory { EmailAddressValidator() }
|
||||
}
|
||||
|
|
140
app/k9mail-jmap/build.gradle
Normal file
140
app/k9mail-jmap/build.gradle
Normal file
|
@ -0,0 +1,140 @@
|
|||
apply plugin: 'com.android.application'
|
||||
apply plugin: 'org.jetbrains.kotlin.android'
|
||||
apply plugin: 'org.jetbrains.kotlin.android.extensions'
|
||||
apply plugin: 'kotlin-kapt'
|
||||
apply plugin: 'org.jlleitschuh.gradle.ktlint'
|
||||
|
||||
if (rootProject.testCoverage) {
|
||||
apply plugin: 'jacoco'
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:${versions.kotlin}"
|
||||
|
||||
implementation project(":app:ui")
|
||||
implementation project(":app:core")
|
||||
implementation project(":app:storage")
|
||||
implementation project(":app:crypto-openpgp")
|
||||
implementation project(":backend:imap")
|
||||
implementation project(":backend:pop3")
|
||||
implementation project(":backend:webdav")
|
||||
implementation project(":backend:jmap")
|
||||
|
||||
implementation "androidx.appcompat:appcompat:${versions.androidxAppCompat}"
|
||||
implementation "com.jakewharton.timber:timber:${versions.timber}"
|
||||
implementation "androidx.constraintlayout:constraintlayout:${versions.androidxConstraintLayout}"
|
||||
implementation "com.google.android.material:material:${versions.materialComponents}"
|
||||
implementation "androidx.navigation:navigation-fragment-ktx:${versions.androidxNavigation}"
|
||||
implementation "androidx.navigation:navigation-ui-ktx:${versions.androidxNavigation}"
|
||||
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.1.0"
|
||||
|
||||
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 "com.nhaarman:mockito-kotlin:${versions.mockitoKotlin}"
|
||||
testImplementation "org.jdom:jdom2:2.0.6"
|
||||
testImplementation "org.koin:koin-test:${versions.koin}"
|
||||
|
||||
androidTestImplementation "androidx.test.espresso:espresso-core:3.1.1"
|
||||
}
|
||||
|
||||
android {
|
||||
compileSdkVersion buildConfig.compileSdk
|
||||
buildToolsVersion buildConfig.buildTools
|
||||
|
||||
defaultConfig {
|
||||
applicationId "com.fsck.k9.jmap"
|
||||
testApplicationId "com.fsck.k9.jmap.tests"
|
||||
|
||||
versionCode 1
|
||||
versionName 'JMAP DEV'
|
||||
|
||||
minSdkVersion buildConfig.minSdk
|
||||
targetSdkVersion buildConfig.targetSdk
|
||||
|
||||
generatedDensities = ['mdpi', 'hdpi', 'xhdpi']
|
||||
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
|
||||
signingConfigs {
|
||||
release
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
if (project.hasProperty('storeFile')) {
|
||||
signingConfig signingConfigs.release
|
||||
}
|
||||
|
||||
minifyEnabled true
|
||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
||||
|
||||
buildConfigField "boolean", "DEVELOPER_MODE", "false"
|
||||
}
|
||||
|
||||
debug {
|
||||
applicationIdSuffix ".debug"
|
||||
testCoverageEnabled rootProject.testCoverage
|
||||
|
||||
minifyEnabled true
|
||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
||||
|
||||
buildConfigField "boolean", "DEVELOPER_MODE", "true"
|
||||
}
|
||||
}
|
||||
|
||||
// Do not abort build if lint finds errors
|
||||
lintOptions {
|
||||
abortOnError false
|
||||
lintConfig file("$rootProject.projectDir/config/lint/lint.xml")
|
||||
}
|
||||
|
||||
packagingOptions {
|
||||
exclude 'META-INF/DEPENDENCIES'
|
||||
exclude 'META-INF/LICENSE'
|
||||
exclude 'META-INF/LICENSE.txt'
|
||||
exclude 'META-INF/NOTICE'
|
||||
exclude 'META-INF/NOTICE.txt'
|
||||
exclude 'META-INF/README'
|
||||
exclude 'LICENSE.txt'
|
||||
exclude 'META-INF/*.kotlin_module'
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility javaVersion
|
||||
targetCompatibility javaVersion
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = kotlinJvmVersion
|
||||
}
|
||||
|
||||
dataBinding {
|
||||
enabled = true
|
||||
}
|
||||
|
||||
testOptions {
|
||||
unitTests {
|
||||
includeAndroidResources = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
androidExtensions {
|
||||
experimental = true
|
||||
}
|
||||
|
||||
if (project.hasProperty('keyAlias')) {
|
||||
android.signingConfigs.release.keyAlias = keyAlias
|
||||
}
|
||||
if (project.hasProperty('keyPassword')) {
|
||||
android.signingConfigs.release.keyPassword = keyPassword
|
||||
}
|
||||
if (project.hasProperty('storeFile')) {
|
||||
android.signingConfigs.release.storeFile = file(storeFile)
|
||||
}
|
||||
if (project.hasProperty('storePassword')) {
|
||||
android.signingConfigs.release.storePassword = storePassword
|
||||
}
|
44
app/k9mail-jmap/proguard-rules.pro
vendored
Normal file
44
app/k9mail-jmap/proguard-rules.pro
vendored
Normal file
|
@ -0,0 +1,44 @@
|
|||
# Add project specific ProGuard rules here.
|
||||
|
||||
-dontobfuscate
|
||||
|
||||
# Preserve the line number information for debugging stack traces.
|
||||
-keepattributes SourceFile,LineNumberTable
|
||||
|
||||
# Library specific rules
|
||||
-dontnote android.net.http.*
|
||||
-dontnote org.apache.commons.codec.**
|
||||
-dontnote org.apache.http.**
|
||||
-dontnote com.squareup.moshi.**
|
||||
-dontnote com.github.amlcurran.showcaseview.**
|
||||
-dontnote de.cketti.safecontentresolver.**
|
||||
-dontnote com.tokenautocomplete.**
|
||||
-keep class rs.ltt.jmap.common.** {*;}
|
||||
|
||||
-dontwarn okio.**
|
||||
-dontwarn com.squareup.moshi.**
|
||||
|
||||
# Project specific rules
|
||||
-dontnote com.fsck.k9.ui.messageview.**
|
||||
-dontnote com.fsck.k9.view.**
|
||||
|
||||
-keep public class org.openintents.openpgp.**
|
||||
|
||||
-keepclassmembers class * extends androidx.appcompat.widget.SearchView {
|
||||
public <init>(android.content.Context);
|
||||
}
|
||||
|
||||
# okhttp rules
|
||||
# see: https://github.com/square/okhttp/blob/master/okhttp/src/main/resources/META-INF/proguard/okhttp3.pro
|
||||
|
||||
# JSR 305 annotations are for embedding nullability information.
|
||||
-dontwarn javax.annotation.**
|
||||
|
||||
# A resource is loaded with a relative path so the package of this class must be preserved.
|
||||
-keepnames class okhttp3.internal.publicsuffix.PublicSuffixDatabase
|
||||
|
||||
# Animal Sniffer compileOnly dependency to ensure APIs are compatible with older versions of Java.
|
||||
-dontwarn org.codehaus.mojo.animal_sniffer.*
|
||||
|
||||
# OkHttp platform used only on JVM and when Conscrypt dependency is available.
|
||||
-dontwarn okhttp3.internal.platform.ConscryptPlatform
|
362
app/k9mail-jmap/src/main/AndroidManifest.xml
Normal file
362
app/k9mail-jmap/src/main/AndroidManifest.xml
Normal file
|
@ -0,0 +1,362 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:installLocation="auto"
|
||||
package="com.fsck.k9.jmap">
|
||||
|
||||
<uses-feature
|
||||
android:name="android.hardware.touchscreen"
|
||||
android:required="false"/>
|
||||
|
||||
<supports-screens
|
||||
android:anyDensity="true"
|
||||
android:largeScreens="true"
|
||||
android:normalScreens="true"
|
||||
android:smallScreens="true"/>
|
||||
|
||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
|
||||
<uses-permission android:name="android.permission.READ_CONTACTS"/>
|
||||
<uses-permission android:name="android.permission.READ_SYNC_SETTINGS"/>
|
||||
|
||||
<!-- Needed to mark a contact as contacted -->
|
||||
<uses-permission android:name="android.permission.WRITE_CONTACTS"/>
|
||||
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
<uses-permission android:name="android.permission.VIBRATE"/>
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK"/>
|
||||
|
||||
<application
|
||||
android:name="com.fsck.k9.App"
|
||||
android:allowTaskReparenting="false"
|
||||
android:usesCleartextTraffic="true"
|
||||
android:icon="@drawable/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:theme="@style/Theme.K9.Startup"
|
||||
android:resizeableActivity="true"
|
||||
android:allowBackup="false">
|
||||
|
||||
<meta-data
|
||||
android:name="android.app.default_searchable"
|
||||
android:value="com.fsck.k9.activity.Search"/>
|
||||
|
||||
<!-- TODO: Remove once minSdkVersion has been changed to 24+ -->
|
||||
<meta-data
|
||||
android:name="com.lge.support.SPLIT_WINDOW"
|
||||
android:value="true"/>
|
||||
<uses-library
|
||||
android:name="com.sec.android.app.multiwindow"
|
||||
android:required="false"/>
|
||||
<meta-data
|
||||
android:name="com.sec.android.support.multiwindow"
|
||||
android:value="true"/>
|
||||
<meta-data
|
||||
android:name="com.samsung.android.sdk.multiwindow.penwindow.enable"
|
||||
android:value="true"/>
|
||||
|
||||
<activity
|
||||
android:name="com.fsck.k9.ui.onboarding.OnboardingActivity"
|
||||
android:label="@string/welcome_message_title" />
|
||||
|
||||
<activity
|
||||
android:name="com.fsck.k9.ui.settings.account.OpenPgpAppSelectDialog"
|
||||
android:configChanges="locale"
|
||||
android:theme="@style/Theme.K9.Transparent"
|
||||
/>
|
||||
|
||||
<activity
|
||||
android:name="com.fsck.k9.activity.setup.FontSizeSettings"
|
||||
android:configChanges="locale"
|
||||
android:label="@string/font_size_settings_title"/>
|
||||
|
||||
<activity
|
||||
android:name="com.fsck.k9.activity.setup.AccountSetupBasics"
|
||||
android:configChanges="locale"
|
||||
android:label="@string/account_setup_basics_title"/>
|
||||
|
||||
<activity
|
||||
android:name="com.fsck.k9.activity.setup.AccountSetupAccountType"
|
||||
android:configChanges="locale"
|
||||
android:label="@string/account_setup_account_type_title"/>
|
||||
|
||||
<activity
|
||||
android:name="com.fsck.k9.activity.setup.AccountSetupIncoming"
|
||||
android:configChanges="locale"
|
||||
android:label="@string/account_setup_incoming_title"/>
|
||||
|
||||
<activity
|
||||
android:name="com.fsck.k9.activity.setup.AccountSetupComposition"
|
||||
android:configChanges="locale"
|
||||
android:label="@string/account_settings_composition_title"/>
|
||||
|
||||
<activity
|
||||
android:name="com.fsck.k9.activity.setup.AccountSetupOutgoing"
|
||||
android:configChanges="locale"
|
||||
android:label="@string/account_setup_outgoing_title"/>
|
||||
|
||||
<activity
|
||||
android:name="com.fsck.k9.activity.setup.AccountSetupOptions"
|
||||
android:configChanges="locale"
|
||||
android:label="@string/account_setup_options_title"/>
|
||||
|
||||
<activity
|
||||
android:name="com.fsck.k9.activity.setup.AccountSetupNames"
|
||||
android:configChanges="locale"
|
||||
android:label="@string/account_setup_names_title"/>
|
||||
|
||||
<activity
|
||||
android:name="com.fsck.k9.activity.ChooseAccount"
|
||||
android:configChanges="locale"
|
||||
android:label="@string/choose_account_title"
|
||||
android:noHistory="true" />
|
||||
|
||||
<activity
|
||||
android:name="com.fsck.k9.ui.choosefolder.ChooseFolderActivity"
|
||||
android:configChanges="locale"
|
||||
android:label="@string/choose_folder_title"
|
||||
android:noHistory="true" />
|
||||
|
||||
<activity
|
||||
android:name="com.fsck.k9.activity.ChooseIdentity"
|
||||
android:configChanges="locale"
|
||||
android:label="@string/choose_identity_title" />
|
||||
|
||||
<activity
|
||||
android:name="com.fsck.k9.activity.ManageIdentities"
|
||||
android:configChanges="locale"
|
||||
android:label="@string/manage_identities_title"/>
|
||||
|
||||
<activity
|
||||
android:name="com.fsck.k9.activity.EditIdentity"
|
||||
android:configChanges="locale"
|
||||
android:label="@string/edit_identity_title"/>
|
||||
|
||||
<activity
|
||||
android:name="com.fsck.k9.activity.NotificationDeleteConfirmation"
|
||||
android:excludeFromRecents="true"
|
||||
android:launchMode="singleTop"
|
||||
android:taskAffinity=""
|
||||
android:theme="@android:style/Theme.Translucent.NoTitleBar"/>
|
||||
|
||||
<!-- XXX Note: this activity is hacked to ignore config changes,
|
||||
since it doesn't currently handle them correctly in code. -->
|
||||
<activity
|
||||
android:name="com.fsck.k9.activity.setup.AccountSetupCheckSettings"
|
||||
android:configChanges="keyboardHidden|orientation|locale"
|
||||
android:label="@string/account_setup_check_settings_title"/>
|
||||
|
||||
<activity
|
||||
android:name="com.fsck.k9.activity.setup.FolderSettings"
|
||||
android:configChanges="locale"
|
||||
android:label="@string/folder_settings_title"
|
||||
android:theme="@android:style/Theme.Dialog"/>
|
||||
|
||||
<activity
|
||||
android:name="com.fsck.k9.ui.endtoend.AutocryptKeyTransferActivity"
|
||||
android:configChanges="locale"
|
||||
android:label="@string/ac_transfer_title"
|
||||
/>
|
||||
|
||||
<activity android:name="com.fsck.k9.activity.MessageList">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN"/>
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
<category android:name="android.intent.category.LAUNCHER"/>
|
||||
|
||||
<!-- TODO: Remove once minSdkVersion has been changed to 24+ -->
|
||||
<category android:name="android.intent.category.MULTIWINDOW_LAUNCHER"/>
|
||||
<category android:name="android.intent.category.PENWINDOW_LAUNCHER"/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name="com.fsck.k9.activity.MessageCompose"
|
||||
android:configChanges="locale"
|
||||
android:enabled="false"
|
||||
android:label="@string/app_name">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.SENDTO"/>
|
||||
<data android:scheme="mailto"/>
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.SEND"/>
|
||||
<data android:mimeType="*/*"/>
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.SEND_MULTIPLE"/>
|
||||
<data android:mimeType="*/*"/>
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW"/>
|
||||
<data android:scheme="mailto"/>
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
<category android:name="android.intent.category.BROWSABLE"/>
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="org.autocrypt.PEER_ACTION"/>
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
</intent-filter>
|
||||
|
||||
<meta-data
|
||||
android:name="android.service.chooser.chooser_target_service"
|
||||
android:value="com.fsck.k9.directshare.K9ChooserTargetService" />
|
||||
</activity>
|
||||
|
||||
<!-- Search Activity - searchable -->
|
||||
<activity
|
||||
android:name="com.fsck.k9.activity.Search"
|
||||
android:configChanges="locale"
|
||||
android:label="@string/search_action"
|
||||
android:uiOptions="splitActionBarWhenNarrow">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.SEARCH"/>
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
</intent-filter>
|
||||
<meta-data
|
||||
android:name="android.app.searchable"
|
||||
android:resource="@xml/searchable"/>
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name="com.fsck.k9.activity.UpgradeDatabases"
|
||||
android:label="@string/upgrade_databases_title"/>
|
||||
|
||||
<activity
|
||||
android:name="com.fsck.k9.ui.managefolders.ManageFoldersActivity"
|
||||
android:label="@string/folders_action" />
|
||||
|
||||
<activity
|
||||
android:name="com.fsck.k9.ui.settings.SettingsActivity"
|
||||
android:label="@string/prefs_title" />
|
||||
|
||||
<activity
|
||||
android:name="com.fsck.k9.ui.settings.general.GeneralSettingsActivity"
|
||||
android:label="@string/general_settings_title" />
|
||||
|
||||
<activity
|
||||
android:name="com.fsck.k9.ui.settings.account.AccountSettingsActivity"
|
||||
android:label="@string/account_settings_title_fmt" />
|
||||
|
||||
<activity
|
||||
android:name="com.fsck.k9.ui.addaccount.AddAccountActivity"
|
||||
android:label="@string/add_account_action" />
|
||||
|
||||
<receiver
|
||||
android:name="com.fsck.k9.service.BootReceiver"
|
||||
android:enabled="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.BOOT_COMPLETED"/>
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.net.conn.BACKGROUND_DATA_SETTING_CHANGED"/>
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="com.android.sync.SYNC_CONN_STATUS_CHANGED"/>
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<receiver
|
||||
android:name="com.fsck.k9.service.CoreReceiver"
|
||||
android:exported="false"/>
|
||||
|
||||
<receiver
|
||||
android:name="com.fsck.k9.service.StorageReceiver"
|
||||
android:enabled="true">
|
||||
<intent-filter>
|
||||
<!--
|
||||
android.intent.action.MEDIA_MOUNTED
|
||||
|
||||
* Broadcast Action: External media is present and mounted at its mount point.
|
||||
* The path to the mount point for the removed media is contained in the Intent.mData field.
|
||||
* The Intent contains an extra with name "read-only" and Boolean value to indicate if the
|
||||
* media was mounted read only.
|
||||
|
||||
-->
|
||||
<action android:name="android.intent.action.MEDIA_MOUNTED"/>
|
||||
<!--
|
||||
|
||||
MEDIA_EJECT and MEDIA_UNMOUNTED are not defined here: they have to be dynamically registered
|
||||
otherwise it would make K-9 start at the wrong time
|
||||
|
||||
-->
|
||||
<data android:scheme="file"/>
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<service
|
||||
android:name="com.fsck.k9.notification.NotificationActionService"
|
||||
android:enabled="true"/>
|
||||
|
||||
<service
|
||||
android:name="com.fsck.k9.service.PushService"
|
||||
android:enabled="true"/>
|
||||
|
||||
<service
|
||||
android:name="com.fsck.k9.service.SleepService"
|
||||
android:enabled="true"/>
|
||||
|
||||
<service
|
||||
android:name="com.fsck.k9.service.DatabaseUpgradeService"
|
||||
android:exported="false"/>
|
||||
|
||||
<service
|
||||
android:name="com.fsck.k9.account.AccountRemoverService"
|
||||
android:permission="android.permission.BIND_JOB_SERVICE"/>
|
||||
|
||||
<provider
|
||||
android:name="com.fsck.k9.provider.AttachmentProvider"
|
||||
android:authorities="${applicationId}.attachmentprovider"
|
||||
android:exported="false"
|
||||
android:grantUriPermissions="true">
|
||||
|
||||
<meta-data
|
||||
android:name="de.cketti.safecontentresolver.ALLOW_INTERNAL_ACCESS"
|
||||
android:value="true" />
|
||||
|
||||
</provider>
|
||||
|
||||
<provider
|
||||
android:name="com.fsck.k9.provider.RawMessageProvider"
|
||||
android:authorities="${applicationId}.rawmessageprovider"
|
||||
android:exported="false">
|
||||
|
||||
<meta-data
|
||||
android:name="de.cketti.safecontentresolver.ALLOW_INTERNAL_ACCESS"
|
||||
android:value="true" />
|
||||
|
||||
</provider>
|
||||
|
||||
<provider
|
||||
android:name="com.fsck.k9.provider.EmailProvider"
|
||||
android:authorities="${applicationId}.provider.email"
|
||||
android:exported="false"/>
|
||||
|
||||
<provider
|
||||
android:name="com.fsck.k9.provider.DecryptedFileProvider"
|
||||
android:authorities="${applicationId}.decryptedfileprovider"
|
||||
android:exported="false"
|
||||
android:grantUriPermissions="true">
|
||||
|
||||
<meta-data
|
||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||
android:resource="@xml/decrypted_file_provider_paths" />
|
||||
</provider>
|
||||
|
||||
<provider
|
||||
android:name="com.fsck.k9.provider.AttachmentTempFileProvider"
|
||||
android:authorities="${applicationId}.tempfileprovider"
|
||||
android:exported="false"
|
||||
android:grantUriPermissions="true">
|
||||
|
||||
<meta-data
|
||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||
android:resource="@xml/temp_file_provider_paths" />
|
||||
</provider>
|
||||
|
||||
</application>
|
||||
</manifest>
|
35
app/k9mail-jmap/src/main/java/com/fsck/k9/App.kt
Normal file
35
app/k9mail-jmap/src/main/java/com/fsck/k9/App.kt
Normal file
|
@ -0,0 +1,35 @@
|
|||
package com.fsck.k9
|
||||
|
||||
import android.app.Application
|
||||
import com.fsck.k9.activity.MessageCompose
|
||||
import com.fsck.k9.controller.MessagingController
|
||||
import com.fsck.k9.ui.ThemeManager
|
||||
import org.koin.android.ext.android.inject
|
||||
|
||||
class App : Application() {
|
||||
private val messagingController: MessagingController by inject()
|
||||
private val messagingListenerProvider: MessagingListenerProvider by inject()
|
||||
private val themeManager: ThemeManager by inject()
|
||||
|
||||
override fun onCreate() {
|
||||
Core.earlyInit(this)
|
||||
|
||||
super.onCreate()
|
||||
|
||||
DI.start(this, coreModules + uiModules + appModules)
|
||||
|
||||
K9.init(this)
|
||||
Core.init(this)
|
||||
themeManager.init()
|
||||
|
||||
messagingListenerProvider.listeners.forEach { listener ->
|
||||
messagingController.addListener(listener)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
val appConfig = AppConfig(
|
||||
componentsToDisable = listOf(MessageCompose::class.java)
|
||||
)
|
||||
}
|
||||
}
|
31
app/k9mail-jmap/src/main/java/com/fsck/k9/Dependencies.kt
Normal file
31
app/k9mail-jmap/src/main/java/com/fsck/k9/Dependencies.kt
Normal file
|
@ -0,0 +1,31 @@
|
|||
package com.fsck.k9
|
||||
|
||||
import com.fsck.k9.backends.backendsModule
|
||||
import com.fsck.k9.controller.ControllerExtension
|
||||
import com.fsck.k9.crypto.EncryptionExtractor
|
||||
import com.fsck.k9.crypto.openpgp.OpenPgpEncryptionExtractor
|
||||
import com.fsck.k9.notification.notificationModule
|
||||
import com.fsck.k9.preferences.K9StoragePersister
|
||||
import com.fsck.k9.preferences.StoragePersister
|
||||
import com.fsck.k9.resources.resourcesModule
|
||||
import com.fsck.k9.storage.storageModule
|
||||
import com.fsck.k9.ui.addaccount.uiAddAccountModule
|
||||
import org.koin.core.qualifier.named
|
||||
import org.koin.dsl.module
|
||||
|
||||
private val mainAppModule = module {
|
||||
single { App.appConfig }
|
||||
single { MessagingListenerProvider(emptyList()) }
|
||||
single(named("controllerExtensions")) { emptyList<ControllerExtension>() }
|
||||
single<EncryptionExtractor> { OpenPgpEncryptionExtractor.newInstance() }
|
||||
single<StoragePersister> { K9StoragePersister(get()) }
|
||||
}
|
||||
|
||||
val appModules = listOf(
|
||||
mainAppModule,
|
||||
notificationModule,
|
||||
resourcesModule,
|
||||
backendsModule,
|
||||
storageModule,
|
||||
uiAddAccountModule
|
||||
)
|
|
@ -0,0 +1,5 @@
|
|||
package com.fsck.k9
|
||||
|
||||
import com.fsck.k9.controller.MessagingListener
|
||||
|
||||
class MessagingListenerProvider(val listeners: List<MessagingListener>)
|
|
@ -0,0 +1,70 @@
|
|||
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
|
||||
import com.fsck.k9.backend.imap.ImapBackend
|
||||
import com.fsck.k9.backend.imap.ImapStoreUriCreator
|
||||
import com.fsck.k9.backend.imap.ImapStoreUriDecoder
|
||||
import com.fsck.k9.mail.ServerSettings
|
||||
import com.fsck.k9.mail.oauth.OAuth2TokenProvider
|
||||
import com.fsck.k9.mail.power.PowerManager
|
||||
import com.fsck.k9.mail.ssl.TrustedSocketFactory
|
||||
import com.fsck.k9.mail.store.imap.ImapStore
|
||||
import com.fsck.k9.mail.transport.smtp.SmtpTransport
|
||||
import com.fsck.k9.mail.transport.smtp.SmtpTransportUriCreator
|
||||
import com.fsck.k9.mail.transport.smtp.SmtpTransportUriDecoder
|
||||
import com.fsck.k9.mailstore.K9BackendStorageFactory
|
||||
|
||||
class ImapBackendFactory(
|
||||
private val context: Context,
|
||||
private val powerManager: PowerManager,
|
||||
private val backendStorageFactory: K9BackendStorageFactory,
|
||||
private val trustedSocketFactory: TrustedSocketFactory
|
||||
) : BackendFactory {
|
||||
override val transportUriPrefix = "smtp"
|
||||
|
||||
override fun createBackend(account: Account): Backend {
|
||||
val accountName = account.displayName
|
||||
val backendStorage = backendStorageFactory.createBackendStorage(account)
|
||||
val imapStore = createImapStore(account)
|
||||
val smtpTransport = createSmtpTransport(account)
|
||||
return ImapBackend(accountName, backendStorage, imapStore, powerManager, smtpTransport)
|
||||
}
|
||||
|
||||
private fun createImapStore(account: Account): ImapStore {
|
||||
val oAuth2TokenProvider: OAuth2TokenProvider? = null
|
||||
val serverSettings = ImapStoreUriDecoder.decode(account.storeUri)
|
||||
return ImapStore(
|
||||
serverSettings,
|
||||
account,
|
||||
trustedSocketFactory,
|
||||
context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager,
|
||||
oAuth2TokenProvider
|
||||
)
|
||||
}
|
||||
|
||||
private fun createSmtpTransport(account: Account): SmtpTransport {
|
||||
val serverSettings = decodeTransportUri(account.transportUri)
|
||||
val oauth2TokenProvider: OAuth2TokenProvider? = null
|
||||
return SmtpTransport(serverSettings, trustedSocketFactory, oauth2TokenProvider)
|
||||
}
|
||||
|
||||
override fun decodeStoreUri(storeUri: String): ServerSettings {
|
||||
return ImapStoreUriDecoder.decode(storeUri)
|
||||
}
|
||||
|
||||
override fun createStoreUri(serverSettings: ServerSettings): String {
|
||||
return ImapStoreUriCreator.create(serverSettings)
|
||||
}
|
||||
|
||||
override fun decodeTransportUri(transportUri: String): ServerSettings {
|
||||
return SmtpTransportUriDecoder.decodeSmtpUri(transportUri)
|
||||
}
|
||||
|
||||
override fun createTransportUri(serverSettings: ServerSettings): String {
|
||||
return SmtpTransportUriCreator.createSmtpUri(serverSettings)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,60 @@
|
|||
package com.fsck.k9.backends
|
||||
|
||||
import com.fsck.k9.Account
|
||||
import com.fsck.k9.Preferences
|
||||
import com.fsck.k9.account.AccountCreator
|
||||
import com.fsck.k9.backend.BackendManager
|
||||
import com.fsck.k9.backend.jmap.JmapDiscoveryResult.JmapAccount
|
||||
import com.fsck.k9.mail.AuthType
|
||||
import com.fsck.k9.mail.ConnectionSecurity
|
||||
import com.fsck.k9.mail.ServerSettings
|
||||
import com.fsck.k9.mailstore.LocalStoreProvider
|
||||
|
||||
class JmapAccountCreator(
|
||||
private val preferences: Preferences,
|
||||
private val backendManager: BackendManager,
|
||||
private val accountCreator: AccountCreator,
|
||||
private val localStoreProvider: LocalStoreProvider
|
||||
) {
|
||||
fun createAccount(emailAddress: String, password: String, jmapAccount: JmapAccount) {
|
||||
val serverSettings = createServerSettings(emailAddress, password, jmapAccount)
|
||||
|
||||
val account = preferences.newAccount().apply {
|
||||
email = emailAddress
|
||||
description = jmapAccount.name
|
||||
storeUri = backendManager.createStoreUri(serverSettings)
|
||||
transportUri = backendManager.createTransportUri(serverSettings)
|
||||
|
||||
chipColor = accountCreator.pickColor()
|
||||
deletePolicy = Account.DeletePolicy.ON_DELETE
|
||||
}
|
||||
preferences.saveAccount(account)
|
||||
|
||||
createOutboxFolder(account)
|
||||
fetchFolderList(account)
|
||||
}
|
||||
|
||||
private fun createServerSettings(emailAddress: String, password: String, jmapAccount: JmapAccount): ServerSettings {
|
||||
return ServerSettings(
|
||||
"jmap",
|
||||
null,
|
||||
433,
|
||||
ConnectionSecurity.SSL_TLS_REQUIRED,
|
||||
AuthType.PLAIN,
|
||||
emailAddress,
|
||||
password,
|
||||
null,
|
||||
mapOf("accountId" to jmapAccount.accountId)
|
||||
)
|
||||
}
|
||||
|
||||
private fun createOutboxFolder(account: Account) {
|
||||
val localStore = localStoreProvider.getInstance(account)
|
||||
localStore.createLocalFolder(Account.OUTBOX, Account.OUTBOX_NAME)
|
||||
}
|
||||
|
||||
private fun fetchFolderList(account: Account) {
|
||||
val backend = backendManager.getBackend(account)
|
||||
backend.refreshFolderList()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,73 @@
|
|||
package com.fsck.k9.backends
|
||||
|
||||
import android.net.Uri
|
||||
import com.fsck.k9.Account
|
||||
import com.fsck.k9.backend.BackendFactory
|
||||
import com.fsck.k9.backend.api.Backend
|
||||
import com.fsck.k9.backend.jmap.JmapBackend
|
||||
import com.fsck.k9.backend.jmap.JmapConfig
|
||||
import com.fsck.k9.mail.AuthType
|
||||
import com.fsck.k9.mail.ConnectionSecurity
|
||||
import com.fsck.k9.mail.ServerSettings
|
||||
import com.fsck.k9.mailstore.K9BackendStorageFactory
|
||||
|
||||
class JmapBackendFactory(private val backendStorageFactory: K9BackendStorageFactory) : BackendFactory {
|
||||
override val transportUriPrefix = "jmap"
|
||||
|
||||
override fun createBackend(account: Account): Backend {
|
||||
val backendStorage = backendStorageFactory.createBackendStorage(account)
|
||||
|
||||
val serverSettings = decodeStoreUri(account.storeUri)
|
||||
val jmapConfig = JmapConfig(
|
||||
username = serverSettings.username,
|
||||
password = serverSettings.password,
|
||||
baseUrl = serverSettings.host,
|
||||
accountId = serverSettings.extra["accountId"]!!
|
||||
)
|
||||
|
||||
return JmapBackend(backendStorage, jmapConfig)
|
||||
}
|
||||
|
||||
override fun decodeStoreUri(storeUri: String): ServerSettings {
|
||||
val uri = Uri.parse(storeUri)
|
||||
val username = uri.getQueryParameter("username")
|
||||
val password = uri.getQueryParameter("password")
|
||||
val baseUrl = uri.getQueryParameter("baseUrl")
|
||||
val accountId = uri.getQueryParameter("accountId")
|
||||
|
||||
val extra = mapOf(
|
||||
"accountId" to accountId
|
||||
)
|
||||
|
||||
return ServerSettings("jmap", baseUrl, 433, ConnectionSecurity.SSL_TLS_REQUIRED, AuthType.PLAIN, username, password, null, extra)
|
||||
}
|
||||
|
||||
override fun createStoreUri(serverSettings: ServerSettings): String {
|
||||
val username = serverSettings.username
|
||||
val password = serverSettings.password
|
||||
val baseUrl = serverSettings.host
|
||||
val accountId = serverSettings.extra["accountId"]
|
||||
|
||||
return Uri.Builder()
|
||||
.scheme("jmap")
|
||||
.authority("unused")
|
||||
.appendQueryParameter("username", username)
|
||||
.appendQueryParameter("password", password)
|
||||
.apply {
|
||||
if (baseUrl != null) {
|
||||
appendQueryParameter("baseUrl", baseUrl)
|
||||
}
|
||||
}
|
||||
.appendQueryParameter("accountId", accountId)
|
||||
.build()
|
||||
.toString()
|
||||
}
|
||||
|
||||
override fun decodeTransportUri(transportUri: String): ServerSettings {
|
||||
return decodeStoreUri(transportUri)
|
||||
}
|
||||
|
||||
override fun createTransportUri(serverSettings: ServerSettings): String {
|
||||
return createStoreUri(serverSettings)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
package com.fsck.k9.backends
|
||||
|
||||
import com.fsck.k9.backend.BackendManager
|
||||
import com.fsck.k9.backend.jmap.JmapAccountDiscovery
|
||||
import org.koin.dsl.module
|
||||
|
||||
val backendsModule = module {
|
||||
single {
|
||||
BackendManager(
|
||||
mapOf(
|
||||
"imap" to get<ImapBackendFactory>(),
|
||||
"pop3" to get<Pop3BackendFactory>(),
|
||||
"webdav" to get<WebDavBackendFactory>(),
|
||||
"jmap" to get<JmapBackendFactory>()
|
||||
))
|
||||
}
|
||||
single { ImapBackendFactory(get(), get(), get(), get()) }
|
||||
single { Pop3BackendFactory(get(), get()) }
|
||||
single { WebDavBackendFactory(get(), get()) }
|
||||
single { JmapBackendFactory(get()) }
|
||||
factory { JmapAccountDiscovery() }
|
||||
factory { JmapAccountCreator(get(), get(), get(), get()) }
|
||||
}
|
|
@ -0,0 +1,58 @@
|
|||
package com.fsck.k9.backends
|
||||
|
||||
import com.fsck.k9.Account
|
||||
import com.fsck.k9.backend.BackendFactory
|
||||
import com.fsck.k9.backend.api.Backend
|
||||
import com.fsck.k9.backend.pop3.Pop3Backend
|
||||
import com.fsck.k9.backend.pop3.Pop3StoreUriCreator
|
||||
import com.fsck.k9.backend.pop3.Pop3StoreUriDecoder
|
||||
import com.fsck.k9.mail.ServerSettings
|
||||
import com.fsck.k9.mail.oauth.OAuth2TokenProvider
|
||||
import com.fsck.k9.mail.ssl.TrustedSocketFactory
|
||||
import com.fsck.k9.mail.store.pop3.Pop3Store
|
||||
import com.fsck.k9.mail.transport.smtp.SmtpTransport
|
||||
import com.fsck.k9.mail.transport.smtp.SmtpTransportUriCreator
|
||||
import com.fsck.k9.mail.transport.smtp.SmtpTransportUriDecoder
|
||||
import com.fsck.k9.mailstore.K9BackendStorageFactory
|
||||
|
||||
class Pop3BackendFactory(
|
||||
private val backendStorageFactory: K9BackendStorageFactory,
|
||||
private val trustedSocketFactory: TrustedSocketFactory
|
||||
) : BackendFactory {
|
||||
override val transportUriPrefix = "smtp"
|
||||
|
||||
override fun createBackend(account: Account): Backend {
|
||||
val accountName = account.displayName
|
||||
val backendStorage = backendStorageFactory.createBackendStorage(account)
|
||||
val pop3Store = createPop3Store(account)
|
||||
val smtpTransport = createSmtpTransport(account)
|
||||
return Pop3Backend(accountName, backendStorage, pop3Store, smtpTransport)
|
||||
}
|
||||
|
||||
private fun createPop3Store(account: Account): Pop3Store {
|
||||
val serverSettings = decodeStoreUri(account.storeUri)
|
||||
return Pop3Store(serverSettings, account, trustedSocketFactory)
|
||||
}
|
||||
|
||||
private fun createSmtpTransport(account: Account): SmtpTransport {
|
||||
val serverSettings = decodeTransportUri(account.transportUri)
|
||||
val oauth2TokenProvider: OAuth2TokenProvider? = null
|
||||
return SmtpTransport(serverSettings, trustedSocketFactory, oauth2TokenProvider)
|
||||
}
|
||||
|
||||
override fun decodeStoreUri(storeUri: String): ServerSettings {
|
||||
return Pop3StoreUriDecoder.decode(storeUri)
|
||||
}
|
||||
|
||||
override fun createStoreUri(serverSettings: ServerSettings): String {
|
||||
return Pop3StoreUriCreator.create(serverSettings)
|
||||
}
|
||||
|
||||
override fun decodeTransportUri(transportUri: String): ServerSettings {
|
||||
return SmtpTransportUriDecoder.decodeSmtpUri(transportUri)
|
||||
}
|
||||
|
||||
override fun createTransportUri(serverSettings: ServerSettings): String {
|
||||
return SmtpTransportUriCreator.createSmtpUri(serverSettings)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,50 @@
|
|||
package com.fsck.k9.backends
|
||||
|
||||
import com.fsck.k9.Account
|
||||
import com.fsck.k9.backend.BackendFactory
|
||||
import com.fsck.k9.backend.api.Backend
|
||||
import com.fsck.k9.backend.webdav.WebDavBackend
|
||||
import com.fsck.k9.backend.webdav.WebDavStoreUriCreator
|
||||
import com.fsck.k9.backend.webdav.WebDavStoreUriDecoder
|
||||
import com.fsck.k9.mail.ServerSettings
|
||||
import com.fsck.k9.mail.ssl.TrustManagerFactory
|
||||
import com.fsck.k9.mail.store.webdav.WebDavStore
|
||||
import com.fsck.k9.mail.store.webdav.WebDavStoreSettings
|
||||
import com.fsck.k9.mail.transport.WebDavTransport
|
||||
import com.fsck.k9.mailstore.K9BackendStorageFactory
|
||||
|
||||
class WebDavBackendFactory(
|
||||
private val backendStorageFactory: K9BackendStorageFactory,
|
||||
private val trustManagerFactory: TrustManagerFactory
|
||||
) : BackendFactory {
|
||||
override val transportUriPrefix = "webdav"
|
||||
|
||||
override fun createBackend(account: Account): Backend {
|
||||
val accountName = account.displayName
|
||||
val backendStorage = backendStorageFactory.createBackendStorage(account)
|
||||
val serverSettings = WebDavStoreUriDecoder.decode(account.storeUri)
|
||||
val webDavStore = createWebDavStore(serverSettings, account)
|
||||
val webDavTransport = WebDavTransport(trustManagerFactory, serverSettings, account)
|
||||
return WebDavBackend(accountName, backendStorage, webDavStore, webDavTransport)
|
||||
}
|
||||
|
||||
private fun createWebDavStore(serverSettings: WebDavStoreSettings, account: Account): WebDavStore {
|
||||
return WebDavStore(trustManagerFactory, serverSettings, account)
|
||||
}
|
||||
|
||||
override fun decodeStoreUri(storeUri: String): ServerSettings {
|
||||
return WebDavStoreUriDecoder.decode(storeUri)
|
||||
}
|
||||
|
||||
override fun createStoreUri(serverSettings: ServerSettings): String {
|
||||
return WebDavStoreUriCreator.create(serverSettings)
|
||||
}
|
||||
|
||||
override fun decodeTransportUri(transportUri: String): ServerSettings {
|
||||
return WebDavStoreUriDecoder.decode(transportUri)
|
||||
}
|
||||
|
||||
override fun createTransportUri(serverSettings: ServerSettings): String {
|
||||
return WebDavStoreUriCreator.create(serverSettings)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,245 @@
|
|||
package com.fsck.k9.notification;
|
||||
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import android.app.PendingIntent;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import com.fsck.k9.Account;
|
||||
import com.fsck.k9.DI;
|
||||
import com.fsck.k9.K9;
|
||||
import com.fsck.k9.activity.MessageList;
|
||||
import com.fsck.k9.activity.NotificationDeleteConfirmation;
|
||||
import com.fsck.k9.activity.compose.MessageActions;
|
||||
import com.fsck.k9.activity.setup.AccountSetupIncoming;
|
||||
import com.fsck.k9.activity.setup.AccountSetupOutgoing;
|
||||
import com.fsck.k9.controller.MessageReference;
|
||||
import com.fsck.k9.jmap.R;
|
||||
import com.fsck.k9.search.AccountSearchConditions;
|
||||
import com.fsck.k9.search.LocalSearch;
|
||||
import com.fsck.k9.ui.messagelist.DefaultFolderProvider;
|
||||
|
||||
|
||||
/**
|
||||
* This class contains methods to create the {@link PendingIntent}s for the actions of our notifications.
|
||||
* <p/>
|
||||
* <strong>Note:</strong>
|
||||
* We need to take special care to ensure the {@code PendingIntent}s are unique as defined in the documentation of
|
||||
* {@link PendingIntent}. Otherwise selecting a notification action might perform the action on the wrong message.
|
||||
* <p/>
|
||||
* We use the notification ID as {@code requestCode} argument to ensure each notification/action pair gets a unique
|
||||
* {@code PendingIntent}.
|
||||
*/
|
||||
class K9NotificationActionCreator implements NotificationActionCreator {
|
||||
private final Context context;
|
||||
private final AccountSearchConditions accountSearchConditions = DI.get(AccountSearchConditions.class);
|
||||
private final DefaultFolderProvider defaultFolderProvider = DI.get(DefaultFolderProvider.class);
|
||||
|
||||
|
||||
public K9NotificationActionCreator(Context context) {
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
@Override
|
||||
public PendingIntent createViewMessagePendingIntent(MessageReference messageReference, int notificationId) {
|
||||
Intent intent = createMessageViewIntent(messageReference);
|
||||
return PendingIntent.getActivity(context, notificationId, intent, PendingIntent.FLAG_UPDATE_CURRENT);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PendingIntent createViewFolderPendingIntent(Account account, String folderServerId, int notificationId) {
|
||||
Intent intent = createMessageListIntent(account, folderServerId);
|
||||
return PendingIntent.getActivity(context, notificationId, intent, PendingIntent.FLAG_UPDATE_CURRENT);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PendingIntent createViewMessagesPendingIntent(Account account, List<MessageReference> messageReferences,
|
||||
int notificationId) {
|
||||
|
||||
Intent intent;
|
||||
if (account.isGoToUnreadMessageSearch()) {
|
||||
intent = createUnreadIntent(account);
|
||||
} else {
|
||||
String folderServerId = getFolderServerIdOfAllMessages(messageReferences);
|
||||
|
||||
if (folderServerId == null) {
|
||||
intent = createMessageListIntent(account);
|
||||
} else {
|
||||
intent = createMessageListIntent(account, folderServerId);
|
||||
}
|
||||
}
|
||||
|
||||
return PendingIntent.getActivity(context, notificationId, intent, PendingIntent.FLAG_UPDATE_CURRENT);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PendingIntent createViewFolderListPendingIntent(Account account, int notificationId) {
|
||||
Intent intent = createMessageListIntent(account);
|
||||
return PendingIntent.getActivity(context, notificationId, intent, PendingIntent.FLAG_UPDATE_CURRENT);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PendingIntent createDismissAllMessagesPendingIntent(Account account, int notificationId) {
|
||||
Intent intent = NotificationActionService.createDismissAllMessagesIntent(context, account);
|
||||
|
||||
return PendingIntent.getService(context, notificationId, intent, PendingIntent.FLAG_UPDATE_CURRENT);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PendingIntent createDismissMessagePendingIntent(Context context, MessageReference messageReference,
|
||||
int notificationId) {
|
||||
|
||||
Intent intent = NotificationActionService.createDismissMessageIntent(context, messageReference);
|
||||
|
||||
return PendingIntent.getService(context, notificationId, intent, PendingIntent.FLAG_UPDATE_CURRENT);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PendingIntent createReplyPendingIntent(MessageReference messageReference, int notificationId) {
|
||||
Intent intent = MessageActions.getActionReplyIntent(context, messageReference);
|
||||
|
||||
return PendingIntent.getActivity(context, notificationId, intent, PendingIntent.FLAG_UPDATE_CURRENT);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PendingIntent createMarkMessageAsReadPendingIntent(MessageReference messageReference, int notificationId) {
|
||||
Intent intent = NotificationActionService.createMarkMessageAsReadIntent(context, messageReference);
|
||||
|
||||
return PendingIntent.getService(context, notificationId, intent, PendingIntent.FLAG_UPDATE_CURRENT);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PendingIntent createMarkAllAsReadPendingIntent(Account account, List<MessageReference> messageReferences,
|
||||
int notificationId) {
|
||||
String accountUuid = account.getUuid();
|
||||
Intent intent = NotificationActionService.createMarkAllAsReadIntent(context, accountUuid, messageReferences);
|
||||
|
||||
return PendingIntent.getService(context, notificationId, intent, PendingIntent.FLAG_UPDATE_CURRENT);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PendingIntent getEditIncomingServerSettingsIntent(Account account) {
|
||||
Intent intent = AccountSetupIncoming.intentActionEditIncomingSettings(context, account);
|
||||
|
||||
return PendingIntent.getActivity(context, account.getAccountNumber(), intent,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PendingIntent getEditOutgoingServerSettingsIntent(Account account) {
|
||||
Intent intent = AccountSetupOutgoing.intentActionEditOutgoingSettings(context, account);
|
||||
|
||||
return PendingIntent.getActivity(context, account.getAccountNumber(), intent,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PendingIntent createDeleteMessagePendingIntent(MessageReference messageReference, int notificationId) {
|
||||
if (K9.isConfirmDeleteFromNotification()) {
|
||||
return createDeleteConfirmationPendingIntent(messageReference, notificationId);
|
||||
} else {
|
||||
return createDeleteServicePendingIntent(messageReference, notificationId);
|
||||
}
|
||||
}
|
||||
|
||||
private PendingIntent createDeleteServicePendingIntent(MessageReference messageReference, int notificationId) {
|
||||
Intent intent = NotificationActionService.createDeleteMessageIntent(context, messageReference);
|
||||
|
||||
return PendingIntent.getService(context, notificationId, intent, PendingIntent.FLAG_UPDATE_CURRENT);
|
||||
}
|
||||
|
||||
private PendingIntent createDeleteConfirmationPendingIntent(MessageReference messageReference, int notificationId) {
|
||||
Intent intent = NotificationDeleteConfirmation.getIntent(context, messageReference);
|
||||
|
||||
return PendingIntent.getActivity(context, notificationId, intent, PendingIntent.FLAG_UPDATE_CURRENT);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PendingIntent createDeleteAllPendingIntent(Account account, List<MessageReference> messageReferences,
|
||||
int notificationId) {
|
||||
if (K9.isConfirmDeleteFromNotification()) {
|
||||
return getDeleteAllConfirmationPendingIntent(messageReferences, notificationId);
|
||||
} else {
|
||||
return getDeleteAllServicePendingIntent(account, messageReferences, notificationId);
|
||||
}
|
||||
}
|
||||
|
||||
private PendingIntent getDeleteAllConfirmationPendingIntent(List<MessageReference> messageReferences,
|
||||
int notificationId) {
|
||||
Intent intent = NotificationDeleteConfirmation.getIntent(context, messageReferences);
|
||||
|
||||
return PendingIntent.getActivity(context, notificationId, intent, PendingIntent.FLAG_CANCEL_CURRENT);
|
||||
}
|
||||
|
||||
private PendingIntent getDeleteAllServicePendingIntent(Account account, List<MessageReference> messageReferences,
|
||||
int notificationId) {
|
||||
String accountUuid = account.getUuid();
|
||||
Intent intent = NotificationActionService.createDeleteAllMessagesIntent(
|
||||
context, accountUuid, messageReferences);
|
||||
|
||||
return PendingIntent.getService(context, notificationId, intent, PendingIntent.FLAG_UPDATE_CURRENT);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PendingIntent createArchiveMessagePendingIntent(MessageReference messageReference, int notificationId) {
|
||||
Intent intent = NotificationActionService.createArchiveMessageIntent(context, messageReference);
|
||||
|
||||
return PendingIntent.getService(context, notificationId, intent, PendingIntent.FLAG_UPDATE_CURRENT);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PendingIntent createArchiveAllPendingIntent(Account account, List<MessageReference> messageReferences,
|
||||
int notificationId) {
|
||||
Intent intent = NotificationActionService.createArchiveAllIntent(context, account, messageReferences);
|
||||
|
||||
return PendingIntent.getService(context, notificationId, intent, PendingIntent.FLAG_UPDATE_CURRENT);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PendingIntent createMarkMessageAsSpamPendingIntent(MessageReference messageReference, int notificationId) {
|
||||
Intent intent = NotificationActionService.createMarkMessageAsSpamIntent(context, messageReference);
|
||||
|
||||
return PendingIntent.getService(context, notificationId, intent, PendingIntent.FLAG_UPDATE_CURRENT);
|
||||
}
|
||||
|
||||
private Intent createUnreadIntent(final Account account) {
|
||||
String searchTitle = context.getString(R.string.search_title, account.getDescription(), context.getString(R.string.unread_modifier));
|
||||
LocalSearch search = accountSearchConditions.createUnreadSearch(account, searchTitle);
|
||||
return MessageList.intentDisplaySearch(context, search, true, false, false);
|
||||
}
|
||||
|
||||
private Intent createMessageListIntent(Account account) {
|
||||
String folderServerId = defaultFolderProvider.getDefaultFolder(account);
|
||||
LocalSearch search = new LocalSearch(folderServerId);
|
||||
search.addAllowedFolder(folderServerId);
|
||||
search.addAccountUuid(account.getUuid());
|
||||
return MessageList.intentDisplaySearch(context, search, false, true, true);
|
||||
}
|
||||
|
||||
private Intent createMessageListIntent(Account account, String folderServerId) {
|
||||
LocalSearch search = new LocalSearch(folderServerId);
|
||||
search.addAllowedFolder(folderServerId);
|
||||
search.addAccountUuid(account.getUuid());
|
||||
return MessageList.intentDisplaySearch(context, search, false, true, true);
|
||||
}
|
||||
|
||||
private Intent createMessageViewIntent(MessageReference message) {
|
||||
return MessageList.actionDisplayMessageIntent(context, message);
|
||||
}
|
||||
|
||||
private String getFolderServerIdOfAllMessages(List<MessageReference> messageReferences) {
|
||||
MessageReference firstMessage = messageReferences.get(0);
|
||||
String folderServerId = firstMessage.getFolderServerId();
|
||||
|
||||
for (MessageReference messageReference : messageReferences) {
|
||||
if (!TextUtils.equals(folderServerId, messageReference.getFolderServerId())) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return folderServerId;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,92 @@
|
|||
package com.fsck.k9.notification
|
||||
|
||||
import android.content.Context
|
||||
import com.fsck.k9.jmap.R
|
||||
|
||||
class K9NotificationResourceProvider(private val context: Context) : NotificationResourceProvider {
|
||||
override val iconWarning: Int = R.drawable.notification_icon_warning
|
||||
override val iconMarkAsRead: Int = R.drawable.notification_action_mark_as_read
|
||||
override val iconDelete: Int = R.drawable.notification_action_delete
|
||||
override val iconReply: Int = R.drawable.notification_action_reply
|
||||
override val iconNewMail: Int = R.drawable.notification_icon_new_mail
|
||||
override val iconSendingMail: Int = R.drawable.ic_notify_check_mail
|
||||
override val iconCheckingMail: Int = R.drawable.ic_notify_check_mail
|
||||
override val wearIconMarkAsRead: Int = R.drawable.ic_opened_envelope_dark
|
||||
override val wearIconDelete: Int = R.drawable.ic_trash_can_dark
|
||||
override val wearIconArchive: Int = R.drawable.ic_archive_dark
|
||||
override val wearIconReplyAll: Int = R.drawable.ic_reply_all_dark
|
||||
override val wearIconMarkAsSpam: Int = R.drawable.ic_alert_octagon_dark
|
||||
|
||||
override val messagesChannelName: String
|
||||
get() = context.getString(R.string.notification_channel_messages_title)
|
||||
override val messagesChannelDescription: String
|
||||
get() = context.getString(R.string.notification_channel_messages_description)
|
||||
override val miscellaneousChannelName: String
|
||||
get() = context.getString(R.string.notification_channel_miscellaneous_title)
|
||||
override val miscellaneousChannelDescription: String
|
||||
get() = context.getString(R.string.notification_channel_miscellaneous_description)
|
||||
|
||||
override fun authenticationErrorTitle(): String =
|
||||
context.getString(R.string.notification_authentication_error_title)
|
||||
|
||||
override fun authenticationErrorBody(accountName: String): String =
|
||||
context.getString(R.string.notification_authentication_error_text, accountName)
|
||||
|
||||
override fun certificateErrorTitle(accountName: String): String =
|
||||
context.getString(R.string.notification_certificate_error_title, accountName)
|
||||
|
||||
override fun certificateErrorBody(): String = context.getString(R.string.notification_certificate_error_text)
|
||||
|
||||
override fun newMailTitle(): String = context.getString(R.string.notification_new_title)
|
||||
|
||||
override fun newMailUnreadMessageCount(unreadMessageCount: Int, accountName: String): String =
|
||||
context.getString(R.string.notification_new_one_account_fmt, unreadMessageCount, accountName)
|
||||
|
||||
override fun newMessagesTitle(newMessagesCount: Int): String =
|
||||
context.resources.getQuantityString(R.plurals.notification_new_messages_title,
|
||||
newMessagesCount, newMessagesCount)
|
||||
|
||||
override fun additionalMessages(overflowMessagesCount: Int, accountName: String): String =
|
||||
context.getString(R.string.notification_additional_messages, overflowMessagesCount, accountName)
|
||||
|
||||
override fun previewEncrypted(): String = context.getString(R.string.preview_encrypted)
|
||||
|
||||
override fun noSubject(): String = context.getString(R.string.general_no_subject)
|
||||
|
||||
override fun recipientDisplayName(recipientDisplayName: String): String =
|
||||
context.getString(R.string.message_to_fmt, recipientDisplayName)
|
||||
|
||||
override fun noSender(): String = context.getString(R.string.general_no_sender)
|
||||
|
||||
override fun sendFailedTitle(): String = context.getString(R.string.send_failure_subject)
|
||||
|
||||
override fun sendingMailTitle(): String = context.getString(R.string.notification_bg_send_title)
|
||||
|
||||
override fun sendingMailBody(accountName: String): String =
|
||||
context.getString(R.string.notification_bg_send_ticker, accountName)
|
||||
|
||||
override fun checkingMailTicker(accountName: String, folderName: String): String =
|
||||
context.getString(R.string.notification_bg_sync_ticker, accountName, folderName)
|
||||
|
||||
override fun checkingMailTitle(): String =
|
||||
context.getString(R.string.notification_bg_sync_title)
|
||||
|
||||
override fun checkingMailSeparator(): String =
|
||||
context.getString(R.string.notification_bg_title_separator)
|
||||
|
||||
override fun actionMarkAsRead(): String = context.getString(R.string.notification_action_mark_as_read)
|
||||
|
||||
override fun actionMarkAllAsRead(): String = context.getString(R.string.notification_action_mark_all_as_read)
|
||||
|
||||
override fun actionDelete(): String = context.getString(R.string.notification_action_delete)
|
||||
|
||||
override fun actionDeleteAll(): String = context.getString(R.string.notification_action_delete_all)
|
||||
|
||||
override fun actionReply(): String = context.getString(R.string.notification_action_reply)
|
||||
|
||||
override fun actionArchive(): String = context.getString(R.string.notification_action_archive)
|
||||
|
||||
override fun actionArchiveAll(): String = context.getString(R.string.notification_action_archive_all)
|
||||
|
||||
override fun actionMarkAsSpam(): String = context.getString(R.string.notification_action_spam)
|
||||
}
|
|
@ -0,0 +1,69 @@
|
|||
package com.fsck.k9.notification
|
||||
|
||||
import com.fsck.k9.Account
|
||||
import com.fsck.k9.K9
|
||||
import com.fsck.k9.helper.Contacts
|
||||
import com.fsck.k9.mail.Flag
|
||||
import com.fsck.k9.mailstore.LocalFolder
|
||||
import com.fsck.k9.mailstore.LocalMessage
|
||||
|
||||
class K9NotificationStrategy(val contacts: Contacts) : NotificationStrategy {
|
||||
|
||||
override fun shouldNotifyForMessage(
|
||||
account: Account,
|
||||
localFolder: LocalFolder,
|
||||
message: LocalMessage,
|
||||
isOldMessage: Boolean
|
||||
): Boolean {
|
||||
|
||||
// If we don't even have an account name, don't show the notification.
|
||||
// (This happens during initial account setup)
|
||||
if (account.name == null) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (K9.isQuietTime && !K9.isNotificationDuringQuietTimeEnabled) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Do not notify if the user does not have notifications enabled or if the message has
|
||||
// been read.
|
||||
if (!account.isNotifyNewMail || message.isSet(Flag.SEEN) || isOldMessage) {
|
||||
return false
|
||||
}
|
||||
|
||||
val aDisplayMode = account.folderDisplayMode
|
||||
val aNotifyMode = account.folderNotifyNewMailMode
|
||||
val fDisplayClass = localFolder.displayClass
|
||||
val fNotifyClass = localFolder.notifyClass
|
||||
|
||||
if (LocalFolder.isModeMismatch(aDisplayMode, fDisplayClass)) {
|
||||
// Never notify a folder that isn't displayed
|
||||
return false
|
||||
}
|
||||
|
||||
if (LocalFolder.isModeMismatch(aNotifyMode, fNotifyClass)) {
|
||||
// Do not notify folders in the wrong class
|
||||
return false
|
||||
}
|
||||
|
||||
// No notification for new messages in Trash, Drafts, Spam or Sent folder.
|
||||
// But do notify if it's the INBOX (see issue 1817).
|
||||
val folder = message.folder
|
||||
if (folder != null) {
|
||||
val folderServerId = folder.serverId
|
||||
if (folderServerId != account.inboxFolder && (folderServerId == account.trashFolder ||
|
||||
folderServerId == account.draftsFolder ||
|
||||
folderServerId == account.spamFolder ||
|
||||
folderServerId == account.sentFolder)) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// Don't notify if the sender address matches one of our identities and the user chose not
|
||||
// to be notified for such messages.
|
||||
return if (account.isAnIdentity(message.from) && !account.isNotifySelfNewMail) {
|
||||
false
|
||||
} else !account.isNotifyContactsMailOnly || contacts.isAnyInContacts(message.from)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
package com.fsck.k9.notification
|
||||
|
||||
import org.koin.dsl.module
|
||||
|
||||
val notificationModule = module {
|
||||
single<NotificationActionCreator> { K9NotificationActionCreator(get()) }
|
||||
single<NotificationResourceProvider> { K9NotificationResourceProvider(get()) }
|
||||
single<NotificationStrategy> { K9NotificationStrategy(get()) }
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
package com.fsck.k9.resources
|
||||
|
||||
import android.content.Context
|
||||
import com.fsck.k9.autocrypt.AutocryptStringProvider
|
||||
import com.fsck.k9.jmap.R
|
||||
|
||||
class K9AutocryptStringProvider(private val context: Context) : AutocryptStringProvider {
|
||||
override fun transferMessageSubject(): String = context.getString(R.string.ac_transfer_msg_subject)
|
||||
override fun transferMessageBody(): String = context.getString(R.string.ac_transfer_msg_body)
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
package com.fsck.k9.resources
|
||||
|
||||
import android.content.Context
|
||||
import com.fsck.k9.CoreResourceProvider
|
||||
import com.fsck.k9.jmap.R
|
||||
|
||||
class K9CoreResourceProvider(private val context: Context) : CoreResourceProvider {
|
||||
override fun defaultSignature(): String = context.getString(R.string.default_signature)
|
||||
override fun defaultIdentityDescription(): String = context.getString(R.string.default_identity_description)
|
||||
|
||||
override fun sendAlternateChooserTitle(): String = context.getString(R.string.send_alternate_chooser_title)
|
||||
|
||||
override fun internalStorageProviderName(): String =
|
||||
context.getString(R.string.local_storage_provider_internal_label)
|
||||
|
||||
override fun externalStorageProviderName(): String =
|
||||
context.getString(R.string.local_storage_provider_external_label)
|
||||
|
||||
override fun contactDisplayNamePrefix(): String = context.getString(R.string.message_to_label)
|
||||
|
||||
override fun messageHeaderFrom(): String = context.getString(R.string.message_compose_quote_header_from)
|
||||
override fun messageHeaderTo(): String = context.getString(R.string.message_compose_quote_header_to)
|
||||
override fun messageHeaderCc(): String = context.getString(R.string.message_compose_quote_header_cc)
|
||||
override fun messageHeaderDate(): String = context.getString(R.string.message_compose_quote_header_send_date)
|
||||
override fun messageHeaderSubject(): String = context.getString(R.string.message_compose_quote_header_subject)
|
||||
override fun messageHeaderSeparator(): String = context.getString(R.string.message_compose_quote_header_separator)
|
||||
|
||||
override fun noSubject(): String = context.getString(R.string.general_no_subject)
|
||||
|
||||
override fun userAgent(): String = context.getString(R.string.message_header_mua)
|
||||
override fun encryptedSubject(): String = context.getString(R.string.encrypted_subject)
|
||||
|
||||
override fun replyHeader(sender: String): String =
|
||||
context.getString(R.string.message_compose_reply_header_fmt, sender)
|
||||
|
||||
override fun replyHeader(sender: String, sentDate: String): String =
|
||||
context.getString(R.string.message_compose_reply_header_fmt_with_date, sentDate, sender)
|
||||
|
||||
override fun searchAllMessagesTitle(): String = context.getString(R.string.search_all_messages_title)
|
||||
override fun searchAllMessagesDetail(): String = context.getString(R.string.search_all_messages_detail)
|
||||
override fun searchUnifiedInboxTitle(): String = context.getString(R.string.integrated_inbox_title)
|
||||
override fun searchUnifiedInboxDetail(): String = context.getString(R.string.integrated_inbox_detail)
|
||||
|
||||
override fun outboxFolderName(): String = context.getString(R.string.special_mailbox_name_outbox)
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
package com.fsck.k9.resources
|
||||
|
||||
import com.fsck.k9.CoreResourceProvider
|
||||
import com.fsck.k9.autocrypt.AutocryptStringProvider
|
||||
import org.koin.dsl.module
|
||||
|
||||
val resourcesModule = module {
|
||||
single<CoreResourceProvider> { K9CoreResourceProvider(get()) }
|
||||
single<AutocryptStringProvider> { K9AutocryptStringProvider(get()) }
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
package com.fsck.k9.ui
|
||||
|
||||
import android.view.View
|
||||
import androidx.databinding.BindingAdapter
|
||||
import com.google.android.material.textfield.TextInputLayout
|
||||
|
||||
@BindingAdapter("isVisible")
|
||||
fun setVisibility(view: View, value: Boolean) {
|
||||
view.visibility = if (value) View.VISIBLE else View.GONE
|
||||
}
|
||||
|
||||
@BindingAdapter("error")
|
||||
fun setError(view: TextInputLayout, value: Int?) {
|
||||
if (value == null) {
|
||||
view.error = null
|
||||
} else {
|
||||
val errorString = view.context.getString(value)
|
||||
view.error = errorString
|
||||
}
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
package com.fsck.k9.ui.addaccount
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.navigation.NavController
|
||||
import androidx.navigation.findNavController
|
||||
import androidx.navigation.ui.AppBarConfiguration
|
||||
import androidx.navigation.ui.setupActionBarWithNavController
|
||||
import com.fsck.k9.activity.K9Activity
|
||||
import com.fsck.k9.jmap.R
|
||||
|
||||
class AddAccountActivity : K9Activity() {
|
||||
private lateinit var navController: NavController
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setLayout(R.layout.activity_add_account)
|
||||
|
||||
initializeActionBar()
|
||||
}
|
||||
|
||||
private fun initializeActionBar() {
|
||||
val appBarConfiguration = AppBarConfiguration(topLevelDestinationIds = setOf(R.id.addJmapAccountScreen))
|
||||
|
||||
navController = findNavController(R.id.nav_host_fragment)
|
||||
setupActionBarWithNavController(navController, appBarConfiguration)
|
||||
}
|
||||
|
||||
override fun onSupportNavigateUp(): Boolean {
|
||||
return navController.navigateUp() || super.onSupportNavigateUp()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,39 @@
|
|||
package com.fsck.k9.ui.addaccount
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import com.fsck.k9.jmap.R
|
||||
import com.fsck.k9.jmap.databinding.FragmentAddAccountBinding
|
||||
import com.fsck.k9.ui.observeNotNull
|
||||
import org.koin.androidx.viewmodel.ext.android.viewModel
|
||||
|
||||
class AddAccountFragment : Fragment() {
|
||||
private val viewModel: AddAccountViewModel by viewModel()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
viewModel.getActionEvents().observeNotNull(this) { handleActionEvents(it) }
|
||||
}
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||
val binding = FragmentAddAccountBinding.inflate(inflater, container, false)
|
||||
binding.lifecycleOwner = this
|
||||
binding.viewModel = viewModel
|
||||
|
||||
return binding.root
|
||||
}
|
||||
|
||||
private fun handleActionEvents(action: Action) {
|
||||
when (action) {
|
||||
is Action.GoToMessageList -> goToMessageList()
|
||||
}
|
||||
}
|
||||
|
||||
private fun goToMessageList() {
|
||||
findNavController().navigate(R.id.action_addJmapAccountScreen_to_messageListScreen)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,157 @@
|
|||
package com.fsck.k9.ui.addaccount
|
||||
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.Transformations
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.fsck.k9.EmailAddressValidator
|
||||
import com.fsck.k9.backend.jmap.JmapAccountDiscovery
|
||||
import com.fsck.k9.backend.jmap.JmapDiscoveryResult
|
||||
import com.fsck.k9.backend.jmap.JmapDiscoveryResult.JmapAccount
|
||||
import com.fsck.k9.backends.JmapAccountCreator
|
||||
import com.fsck.k9.helper.SingleLiveEvent
|
||||
import com.fsck.k9.helper.measureRealtimeMillisWithResult
|
||||
import com.fsck.k9.jmap.R
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class AddAccountViewModel(
|
||||
private val emailAddressValidator: EmailAddressValidator,
|
||||
private val jmapAccountDiscovery: JmapAccountDiscovery,
|
||||
private val jmapAccountCreator: JmapAccountCreator
|
||||
) : ViewModel() {
|
||||
val emailAddress = MutableLiveData<String>()
|
||||
val emailAddressError = MutableLiveData<Int?>()
|
||||
val password = MutableLiveData<String>()
|
||||
val passwordError = MutableLiveData<Int?>()
|
||||
val setupErrorText: MutableLiveData<Int> = createMutableLiveData(R.string.empty_string)
|
||||
val isInputEnabled: MutableLiveData<Boolean> = createMutableLiveData(true)
|
||||
val isNextButtonEnabled: MutableLiveData<Boolean> = createMutableLiveData(true)
|
||||
val isProgressBarVisible: MutableLiveData<Boolean> = createMutableLiveData(false)
|
||||
private val actionLiveData = SingleLiveEvent<Action>()
|
||||
|
||||
init {
|
||||
Transformations.distinctUntilChanged(emailAddress).observeForever { resetEmailAddressError() }
|
||||
Transformations.distinctUntilChanged(password).observeForever { resetPasswordError() }
|
||||
}
|
||||
|
||||
fun getActionEvents(): LiveData<Action> = actionLiveData
|
||||
|
||||
fun onNextButtonClicked() {
|
||||
discoverServerSettings()
|
||||
}
|
||||
|
||||
private fun discoverServerSettings() {
|
||||
val emailAddress = this.emailAddress.value?.trim() ?: ""
|
||||
val password = this.password.value ?: ""
|
||||
|
||||
if (!emailAddressValidator.isValidAddressOnly(emailAddress)) {
|
||||
displayEmailAddressError(R.string.add_account__email_address_error)
|
||||
return
|
||||
}
|
||||
|
||||
showDiscoveryProgressBar()
|
||||
|
||||
viewModelScope.launch {
|
||||
val (elapsed, discoveryResult) = measureRealtimeMillisWithResult {
|
||||
withContext(Dispatchers.IO) {
|
||||
jmapAccountDiscovery.discover(emailAddress, password)
|
||||
}
|
||||
}
|
||||
|
||||
if (elapsed < MIN_PROGRESS_DURATION) {
|
||||
delay(MIN_PROGRESS_DURATION - elapsed)
|
||||
}
|
||||
|
||||
if (discoveryResult is JmapAccount) {
|
||||
createAccount(emailAddress, password, discoveryResult)
|
||||
} else {
|
||||
displayDiscoveryError(discoveryResult)
|
||||
hideDiscoveryProgressBar()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun createAccount(emailAddress: String, password: String, jmapAccount: JmapAccount) {
|
||||
GlobalScope.launch(Dispatchers.IO) {
|
||||
jmapAccountCreator.createAccount(emailAddress, password, jmapAccount)
|
||||
}.join()
|
||||
|
||||
sendActionEvent(Action.GoToMessageList)
|
||||
}
|
||||
|
||||
private fun displayDiscoveryError(discoveryResult: JmapDiscoveryResult) {
|
||||
when (discoveryResult) {
|
||||
is JmapDiscoveryResult.GenericFailure -> {
|
||||
displayError(R.string.add_account__generic_failure)
|
||||
}
|
||||
is JmapDiscoveryResult.NoEmailAccountFoundFailure -> {
|
||||
displayError(R.string.add_account__no_email_account_found)
|
||||
}
|
||||
is JmapDiscoveryResult.AuthenticationFailure -> {
|
||||
displayPasswordError(R.string.add_account__password_error)
|
||||
}
|
||||
is JmapDiscoveryResult.EndpointNotFoundFailure -> {
|
||||
displayError(R.string.add_account__jmap_server_not_found)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("SameParameterValue")
|
||||
private fun displayEmailAddressError(@StringRes error: Int) {
|
||||
emailAddressError.value = error
|
||||
}
|
||||
|
||||
private fun resetEmailAddressError() {
|
||||
emailAddressError.value = null
|
||||
}
|
||||
|
||||
@Suppress("SameParameterValue")
|
||||
private fun displayPasswordError(@StringRes error: Int) {
|
||||
passwordError.value = error
|
||||
}
|
||||
|
||||
private fun resetPasswordError() {
|
||||
passwordError.value = null
|
||||
}
|
||||
|
||||
private fun showDiscoveryProgressBar() {
|
||||
isInputEnabled.value = false
|
||||
isProgressBarVisible.value = true
|
||||
isNextButtonEnabled.value = false
|
||||
setupErrorText.value = R.string.empty_string
|
||||
}
|
||||
|
||||
private fun hideDiscoveryProgressBar() {
|
||||
isInputEnabled.value = true
|
||||
isProgressBarVisible.value = false
|
||||
isNextButtonEnabled.value = true
|
||||
}
|
||||
|
||||
private fun displayError(@StringRes error: Int) {
|
||||
setupErrorText.value = error
|
||||
}
|
||||
|
||||
private fun sendActionEvent(action: Action) {
|
||||
actionLiveData.value = action
|
||||
}
|
||||
|
||||
private fun <T> createMutableLiveData(initialValue: T): MutableLiveData<T> {
|
||||
return MutableLiveData<T>().apply {
|
||||
value = initialValue
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val MIN_PROGRESS_DURATION = 500
|
||||
}
|
||||
}
|
||||
|
||||
sealed class Action {
|
||||
object GoToMessageList : Action()
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
package com.fsck.k9.ui.addaccount
|
||||
|
||||
import org.koin.androidx.viewmodel.dsl.viewModel
|
||||
import org.koin.dsl.module
|
||||
|
||||
val uiAddAccountModule = module {
|
||||
viewModel { AddAccountViewModel(get(), get(), get()) }
|
||||
}
|
20
app/k9mail-jmap/src/main/res/layout/activity_add_account.xml
Normal file
20
app/k9mail-jmap/src/main/res/layout/activity_add_account.xml
Normal file
|
@ -0,0 +1,20 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical">
|
||||
|
||||
<include layout="@layout/toolbar" />
|
||||
|
||||
<fragment
|
||||
android:id="@+id/nav_host_fragment"
|
||||
android:name="androidx.navigation.fragment.NavHostFragment"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1"
|
||||
app:defaultNavHost="true"
|
||||
app:navGraph="@navigation/navigation_add_account" />
|
||||
|
||||
</LinearLayout>
|
117
app/k9mail-jmap/src/main/res/layout/fragment_add_account.xml
Normal file
117
app/k9mail-jmap/src/main/res/layout/fragment_add_account.xml
Normal file
|
@ -0,0 +1,117 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<data>
|
||||
<variable
|
||||
name="viewModel"
|
||||
type="com.fsck.k9.ui.addaccount.AddAccountViewModel" />
|
||||
</data>
|
||||
|
||||
<ScrollView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:fillViewport="true"
|
||||
android:fitsSystemWindows="true">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/emailAddressLayout"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="16dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
app:error="@{viewModel.emailAddressError}"
|
||||
app:errorEnabled="true"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/emailAddress"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:enabled="@{viewModel.isInputEnabled}"
|
||||
android:hint="@string/account_setup_basics_email_hint"
|
||||
android:imeOptions="flagNoExtractUi"
|
||||
android:inputType="textEmailAddress"
|
||||
android:text="@={viewModel.emailAddress}" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/emailPasswordLayout"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
app:error="@{viewModel.passwordError}"
|
||||
app:errorEnabled="true"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/emailAddressLayout"
|
||||
app:passwordToggleEnabled="true">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/emailPassword"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:enabled="@{viewModel.isInputEnabled}"
|
||||
android:hint="@string/account_setup_basics_password_hint"
|
||||
android:imeOptions="flagNoExtractUi"
|
||||
android:inputType="textPassword"
|
||||
android:text="@={viewModel.password}" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/setupError"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="16dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:text="@{viewModel.setupErrorText}"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/emailPasswordLayout" />
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/progressBar"
|
||||
style="?android:attr/progressBarStyle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="32dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:layout_marginBottom="32dp"
|
||||
app:isVisible="@{viewModel.isProgressBarVisible()}"
|
||||
app:layout_constraintBottom_toTopOf="@+id/nextButton"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/setupError" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/nextButton"
|
||||
style="@style/Widget.AppCompat.Button.Colored"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="24dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:enabled="@{viewModel.isNextButtonEnabled()}"
|
||||
android:onClick="@{() -> viewModel.onNextButtonClicked()}"
|
||||
android:text="@string/next_action"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/emailPasswordLayout"
|
||||
app:layout_constraintVertical_bias="1.0" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</ScrollView>
|
||||
</layout>
|
|
@ -0,0 +1,25 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/navigation_add_account"
|
||||
app:startDestination="@id/addJmapAccountScreen">
|
||||
|
||||
<fragment
|
||||
android:id="@+id/addJmapAccountScreen"
|
||||
android:name="com.fsck.k9.ui.addaccount.AddAccountFragment"
|
||||
android:label="@string/add_account_action"
|
||||
tools:layout="@layout/fragment_add_account">
|
||||
|
||||
<action
|
||||
android:id="@+id/action_addJmapAccountScreen_to_messageListScreen"
|
||||
app:destination="@id/messageListScreen" />
|
||||
|
||||
</fragment>
|
||||
|
||||
<activity
|
||||
android:id="@+id/messageListScreen"
|
||||
android:name="com.fsck.k9.activity.MessageList"
|
||||
tools:layout="@layout/message_list"/>
|
||||
|
||||
</navigation>
|
|
@ -0,0 +1,45 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/navigation_onboarding"
|
||||
app:startDestination="@id/welcomeScreen">
|
||||
|
||||
<fragment
|
||||
android:id="@+id/welcomeScreen"
|
||||
android:name="com.fsck.k9.ui.onboarding.WelcomeFragment"
|
||||
android:label="@string/welcome_message_title"
|
||||
tools:layout="@layout/fragment_welcome_message">
|
||||
|
||||
<action
|
||||
android:id="@+id/action_welcomeScreen_to_settingsImportScreen"
|
||||
app:destination="@id/settingsImportScreen" />
|
||||
|
||||
<action
|
||||
android:id="@+id/action_welcomeScreen_to_addAccountScreen"
|
||||
app:destination="@id/addAccountScreen" />
|
||||
|
||||
<action
|
||||
android:id="@+id/action_welcomeScreen_to_messageListScreen"
|
||||
app:destination="@id/messageListScreen" />
|
||||
|
||||
</fragment>
|
||||
|
||||
<fragment
|
||||
android:id="@+id/settingsImportScreen"
|
||||
android:name="com.fsck.k9.ui.settings.import.SettingsImportFragment"
|
||||
android:label="@string/settings_import_title"
|
||||
tools:layout="@layout/fragment_settings_import"/>
|
||||
|
||||
<activity
|
||||
android:id="@+id/addAccountScreen"
|
||||
android:name="com.fsck.k9.ui.addaccount.AddAccountActivity"
|
||||
android:label="@string/add_account_action"
|
||||
tools:layout="@layout/activity_add_account"/>
|
||||
|
||||
<activity
|
||||
android:id="@+id/messageListScreen"
|
||||
android:name="com.fsck.k9.activity.MessageList"
|
||||
tools:layout="@layout/message_list"/>
|
||||
|
||||
</navigation>
|
11
app/k9mail-jmap/src/main/res/values/strings.xml
Normal file
11
app/k9mail-jmap/src/main/res/values/strings.xml
Normal file
|
@ -0,0 +1,11 @@
|
|||
<?xml version='1.0' encoding='UTF-8'?>
|
||||
<resources>
|
||||
<string name="app_name">K-9 JMAP</string>
|
||||
<string name="empty_string" translatable="false" />
|
||||
|
||||
<string name="add_account__email_address_error">Please enter a valid email address</string>
|
||||
<string name="add_account__password_error">"Couldn't sign in. Please make sure your password is correct."</string>
|
||||
<string name="add_account__generic_failure">Unknown error while discovering server settings</string>
|
||||
<string name="add_account__no_email_account_found">"Couldn't find an email account on the JMAP server"</string>
|
||||
<string name="add_account__jmap_server_not_found">"Couldn't find the JMAP server from your email address"</string>
|
||||
</resources>
|
|
@ -0,0 +1,37 @@
|
|||
package com.fsck.k9
|
||||
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import com.fsck.k9.ui.endtoend.AutocryptKeyTransferActivity
|
||||
import com.fsck.k9.ui.endtoend.AutocryptKeyTransferPresenter
|
||||
import com.nhaarman.mockito_kotlin.doReturn
|
||||
import com.nhaarman.mockito_kotlin.mock
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.koin.core.KoinApplication
|
||||
import org.koin.core.logger.PrintLogger
|
||||
import org.koin.core.parameter.parametersOf
|
||||
import org.koin.test.AutoCloseKoinTest
|
||||
import org.koin.test.check.checkModules
|
||||
import org.openintents.openpgp.OpenPgpApiManager
|
||||
import org.robolectric.RobolectricTestRunner
|
||||
import org.robolectric.annotation.Config
|
||||
|
||||
@RunWith(RobolectricTestRunner::class)
|
||||
@Config(application = App::class)
|
||||
class DependencyInjectionTest : AutoCloseKoinTest() {
|
||||
val lifecycleOwner = mock<LifecycleOwner> {
|
||||
on { lifecycle } doReturn mock<Lifecycle>()
|
||||
}
|
||||
val autocryptTransferView = mock<AutocryptKeyTransferActivity>()
|
||||
|
||||
@Test
|
||||
fun testDependencyTree() {
|
||||
KoinApplication.logger = PrintLogger()
|
||||
|
||||
getKoin().checkModules {
|
||||
create<OpenPgpApiManager> { parametersOf(lifecycleOwner) }
|
||||
create<AutocryptKeyTransferPresenter> { parametersOf(lifecycleOwner, autocryptTransferView) }
|
||||
}
|
||||
}
|
||||
}
|
50
backend/jmap/build.gradle
Normal file
50
backend/jmap/build.gradle
Normal file
|
@ -0,0 +1,50 @@
|
|||
apply plugin: 'com.android.library'
|
||||
apply plugin: 'org.jetbrains.kotlin.android'
|
||||
apply plugin: 'org.jlleitschuh.gradle.ktlint'
|
||||
|
||||
if (rootProject.testCoverage) {
|
||||
apply plugin: 'jacoco'
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:${versions.kotlin}"
|
||||
|
||||
api project(":backend:api")
|
||||
|
||||
implementation 'rs.ltt.jmap:jmap-client:0.2.2'
|
||||
implementation "com.jakewharton.timber:timber:${versions.timber}"
|
||||
|
||||
testImplementation project(":mail:testing")
|
||||
testImplementation "junit:junit:${versions.junit}"
|
||||
testImplementation "org.mockito:mockito-core:${versions.mockito}"
|
||||
testImplementation("com.squareup.okhttp3:mockwebserver:4.2.1")
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
2
backend/jmap/src/main/AndroidManifest.xml
Normal file
2
backend/jmap/src/main/AndroidManifest.xml
Normal file
|
@ -0,0 +1,2 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest package="com.fsck.k9.backend.jmap" />
|
|
@ -0,0 +1,157 @@
|
|||
package com.fsck.k9.backend.jmap
|
||||
|
||||
import com.fsck.k9.backend.api.BackendStorage
|
||||
import com.fsck.k9.backend.api.FolderInfo
|
||||
import com.fsck.k9.mail.AuthenticationFailedException
|
||||
import com.fsck.k9.mail.FolderType
|
||||
import com.fsck.k9.mail.MessagingException
|
||||
import java.lang.Exception
|
||||
import rs.ltt.jmap.client.JmapClient
|
||||
import rs.ltt.jmap.client.api.ErrorResponseException
|
||||
import rs.ltt.jmap.client.api.InvalidSessionResourceException
|
||||
import rs.ltt.jmap.client.api.MethodErrorResponseException
|
||||
import rs.ltt.jmap.client.api.UnauthorizedException
|
||||
import rs.ltt.jmap.common.Request.Invocation.ResultReference
|
||||
import rs.ltt.jmap.common.entity.Mailbox
|
||||
import rs.ltt.jmap.common.entity.Role
|
||||
import rs.ltt.jmap.common.method.call.mailbox.ChangesMailboxMethodCall
|
||||
import rs.ltt.jmap.common.method.call.mailbox.GetMailboxMethodCall
|
||||
import rs.ltt.jmap.common.method.response.mailbox.ChangesMailboxMethodResponse
|
||||
import rs.ltt.jmap.common.method.response.mailbox.GetMailboxMethodResponse
|
||||
|
||||
internal class CommandRefreshFolderList(
|
||||
private val backendStorage: BackendStorage,
|
||||
private val jmapClient: JmapClient,
|
||||
private val accountId: String
|
||||
) {
|
||||
fun refreshFolderList() {
|
||||
try {
|
||||
val state = backendStorage.getExtraString(STATE)
|
||||
if (state == null) {
|
||||
fetchMailboxes()
|
||||
} else {
|
||||
fetchMailboxUpdates(state)
|
||||
}
|
||||
} catch (e: UnauthorizedException) {
|
||||
throw AuthenticationFailedException("Authentication failed", e)
|
||||
} catch (e: InvalidSessionResourceException) {
|
||||
throw MessagingException(e.message, true, e)
|
||||
} catch (e: ErrorResponseException) {
|
||||
throw MessagingException(e.message, true, e)
|
||||
} catch (e: MethodErrorResponseException) {
|
||||
throw MessagingException(e.message, e.isPermanentError, e)
|
||||
} catch (e: Exception) {
|
||||
throw MessagingException(e)
|
||||
}
|
||||
}
|
||||
|
||||
private fun fetchMailboxes() {
|
||||
val call = jmapClient.call(GetMailboxMethodCall(accountId))
|
||||
val response = call.getMainResponseBlocking<GetMailboxMethodResponse>()
|
||||
val foldersOnServer = response.list
|
||||
|
||||
val oldFolderServerIds = backendStorage.getFolderServerIds()
|
||||
val (foldersToUpdate, foldersToCreate) = foldersOnServer.partition { it.id in oldFolderServerIds }
|
||||
|
||||
for (folder in foldersToUpdate) {
|
||||
backendStorage.changeFolder(folder.id, folder.name, folder.type)
|
||||
}
|
||||
|
||||
val newFolders = foldersToCreate.map { folder ->
|
||||
FolderInfo(folder.id, folder.name, folder.type)
|
||||
}
|
||||
backendStorage.createFolders(newFolders)
|
||||
|
||||
val newFolderServerIds = foldersOnServer.map { it.id }
|
||||
val removedFolderServerIds = oldFolderServerIds - newFolderServerIds
|
||||
backendStorage.deleteFolders(removedFolderServerIds)
|
||||
|
||||
backendStorage.setExtraString(STATE, response.state)
|
||||
}
|
||||
|
||||
private fun fetchMailboxUpdates(state: String) {
|
||||
try {
|
||||
fetchAllMailboxChanges(state)
|
||||
} catch (e: MethodErrorResponseException) {
|
||||
if (e.methodErrorResponse.type == ERROR_CANNOT_CALCULATE_CHANGES) {
|
||||
fetchMailboxes()
|
||||
} else {
|
||||
throw e
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun fetchAllMailboxChanges(state: String) {
|
||||
var currentState = state
|
||||
do {
|
||||
val (newState, hasMoreChanges) = fetchMailboxChanges(currentState)
|
||||
currentState = newState
|
||||
} while (hasMoreChanges)
|
||||
}
|
||||
|
||||
private fun fetchMailboxChanges(state: String): UpdateState {
|
||||
val multiCall = jmapClient.newMultiCall()
|
||||
val mailboxChangesCall = multiCall.call(ChangesMailboxMethodCall(accountId, state))
|
||||
val createdMailboxesCall = multiCall.call(
|
||||
GetMailboxMethodCall(
|
||||
accountId,
|
||||
mailboxChangesCall.createResultReference(ResultReference.Path.CREATED)
|
||||
)
|
||||
)
|
||||
val changedMailboxesCall = multiCall.call(
|
||||
GetMailboxMethodCall(
|
||||
accountId,
|
||||
mailboxChangesCall.createResultReference(ResultReference.Path.UPDATED),
|
||||
mailboxChangesCall.createResultReference(ResultReference.Path.UPDATED_PROPERTIES)
|
||||
)
|
||||
)
|
||||
multiCall.execute()
|
||||
|
||||
val mailboxChangesResponse = mailboxChangesCall.getMainResponseBlocking<ChangesMailboxMethodResponse>()
|
||||
val createdMailboxResponse = createdMailboxesCall.getMainResponseBlocking<GetMailboxMethodResponse>()
|
||||
val changedMailboxResponse = changedMailboxesCall.getMainResponseBlocking<GetMailboxMethodResponse>()
|
||||
|
||||
val foldersToCreate = createdMailboxResponse.list.map { folder ->
|
||||
FolderInfo(folder.id, folder.name, folder.type)
|
||||
}
|
||||
backendStorage.createFolders(foldersToCreate)
|
||||
|
||||
for (folder in changedMailboxResponse.list) {
|
||||
backendStorage.changeFolder(folder.id, folder.name, folder.type)
|
||||
}
|
||||
|
||||
val destroyed = mailboxChangesResponse.destroyed
|
||||
destroyed?.let {
|
||||
backendStorage.deleteFolders(it.toList())
|
||||
}
|
||||
|
||||
backendStorage.setExtraString(STATE, mailboxChangesResponse.newState)
|
||||
|
||||
return UpdateState(
|
||||
state = mailboxChangesResponse.newState,
|
||||
hasMoreChanges = mailboxChangesResponse.isHasMoreChanges
|
||||
)
|
||||
}
|
||||
|
||||
private val Mailbox.type: FolderType
|
||||
get() = when (role) {
|
||||
Role.INBOX -> FolderType.INBOX
|
||||
Role.ARCHIVE -> FolderType.ARCHIVE
|
||||
Role.DRAFTS -> FolderType.DRAFTS
|
||||
Role.SENT -> FolderType.SENT
|
||||
Role.TRASH -> FolderType.TRASH
|
||||
Role.JUNK -> FolderType.SPAM
|
||||
else -> FolderType.REGULAR
|
||||
}
|
||||
|
||||
private val MethodErrorResponseException.isPermanentError: Boolean
|
||||
get() = methodErrorResponse.type != ERROR_SERVER_UNAVAILABLE
|
||||
|
||||
companion object {
|
||||
private const val STATE = "jmapState"
|
||||
private const val ERROR_SERVER_UNAVAILABLE = "serverUnavailable"
|
||||
private const val ERROR_CANNOT_CALCULATE_CHANGES = "cannotCalculateChanges"
|
||||
}
|
||||
|
||||
private data class UpdateState(val state: String, val hasMoreChanges: Boolean)
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
package com.fsck.k9.backend.jmap
|
||||
|
||||
import java.net.UnknownHostException
|
||||
import rs.ltt.jmap.client.JmapClient
|
||||
import rs.ltt.jmap.client.api.EndpointNotFoundException
|
||||
import rs.ltt.jmap.client.api.UnauthorizedException
|
||||
import rs.ltt.jmap.common.entity.capability.MailAccountCapability
|
||||
import timber.log.Timber
|
||||
|
||||
class JmapAccountDiscovery {
|
||||
fun discover(emailAddress: String, password: String): JmapDiscoveryResult {
|
||||
val jmapClient = JmapClient(emailAddress, password)
|
||||
val session = try {
|
||||
jmapClient.session.futureGetOrThrow()
|
||||
} catch (e: EndpointNotFoundException) {
|
||||
return JmapDiscoveryResult.EndpointNotFoundFailure
|
||||
} catch (e: UnknownHostException) {
|
||||
return JmapDiscoveryResult.EndpointNotFoundFailure
|
||||
} catch (e: UnauthorizedException) {
|
||||
return JmapDiscoveryResult.AuthenticationFailure
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "Unable to get JMAP session")
|
||||
return JmapDiscoveryResult.GenericFailure(e)
|
||||
}
|
||||
|
||||
val accounts = session.getAccounts(MailAccountCapability::class.java)
|
||||
val accountId = when {
|
||||
accounts.isEmpty() -> return JmapDiscoveryResult.NoEmailAccountFoundFailure
|
||||
accounts.size == 1 -> accounts.keys.first()
|
||||
else -> session.getPrimaryAccount(MailAccountCapability::class.java)
|
||||
}
|
||||
|
||||
val account = accounts[accountId]!!
|
||||
val accountName = account.name ?: emailAddress
|
||||
return JmapDiscoveryResult.JmapAccount(accountId, accountName)
|
||||
}
|
||||
}
|
||||
|
||||
sealed class JmapDiscoveryResult {
|
||||
class GenericFailure(val cause: Throwable) : JmapDiscoveryResult()
|
||||
object EndpointNotFoundFailure : JmapDiscoveryResult()
|
||||
object AuthenticationFailure : JmapDiscoveryResult()
|
||||
object NoEmailAccountFoundFailure : JmapDiscoveryResult()
|
||||
|
||||
data class JmapAccount(val accountId: String, val name: String) : JmapDiscoveryResult()
|
||||
}
|
|
@ -0,0 +1,127 @@
|
|||
package com.fsck.k9.backend.jmap
|
||||
|
||||
import com.fsck.k9.backend.api.Backend
|
||||
import com.fsck.k9.backend.api.BackendStorage
|
||||
import com.fsck.k9.backend.api.SyncConfig
|
||||
import com.fsck.k9.backend.api.SyncListener
|
||||
import com.fsck.k9.mail.BodyFactory
|
||||
import com.fsck.k9.mail.FetchProfile
|
||||
import com.fsck.k9.mail.Flag
|
||||
import com.fsck.k9.mail.Message
|
||||
import com.fsck.k9.mail.Part
|
||||
import com.fsck.k9.mail.PushReceiver
|
||||
import com.fsck.k9.mail.Pusher
|
||||
import okhttp3.HttpUrl
|
||||
import rs.ltt.jmap.client.JmapClient
|
||||
import rs.ltt.jmap.common.method.call.core.EchoMethodCall
|
||||
|
||||
class JmapBackend(
|
||||
private val backendStorage: BackendStorage,
|
||||
config: JmapConfig
|
||||
) : Backend {
|
||||
private val jmapClient = config.toJmapClient()
|
||||
private val accountId = config.accountId
|
||||
private val commandRefreshFolderList = CommandRefreshFolderList(backendStorage, jmapClient, accountId)
|
||||
override val supportsSeenFlag = true
|
||||
override val supportsExpunge = false
|
||||
override val supportsMove = true
|
||||
override val supportsCopy = true
|
||||
override val supportsUpload = true
|
||||
override val supportsTrashFolder = true
|
||||
override val supportsSearchByDate = true
|
||||
override val isPushCapable = false // FIXME
|
||||
override val isDeleteMoveToTrash = false
|
||||
|
||||
override fun refreshFolderList() {
|
||||
commandRefreshFolderList.refreshFolderList()
|
||||
}
|
||||
|
||||
override fun sync(folder: String, syncConfig: SyncConfig, listener: SyncListener) {
|
||||
// TODO: implement
|
||||
}
|
||||
|
||||
override fun downloadMessage(syncConfig: SyncConfig, folderServerId: String, messageServerId: String) {
|
||||
throw UnsupportedOperationException("not implemented")
|
||||
}
|
||||
|
||||
override fun setFlag(folderServerId: String, messageServerIds: List<String>, flag: Flag, newState: Boolean) {
|
||||
throw UnsupportedOperationException("not implemented")
|
||||
}
|
||||
|
||||
override fun markAllAsRead(folderServerId: String) {
|
||||
throw UnsupportedOperationException("not implemented")
|
||||
}
|
||||
|
||||
override fun expunge(folderServerId: String) {
|
||||
throw UnsupportedOperationException("not implemented")
|
||||
}
|
||||
|
||||
override fun expungeMessages(folderServerId: String, messageServerIds: List<String>) {
|
||||
throw UnsupportedOperationException("not implemented")
|
||||
}
|
||||
|
||||
override fun deleteMessages(folderServerId: String, messageServerIds: List<String>) {
|
||||
throw UnsupportedOperationException("not implemented")
|
||||
}
|
||||
|
||||
override fun deleteAllMessages(folderServerId: String) {
|
||||
throw UnsupportedOperationException("not implemented")
|
||||
}
|
||||
|
||||
override fun moveMessages(sourceFolderServerId: String, targetFolderServerId: String, messageServerIds: List<String>): Map<String, String>? {
|
||||
throw UnsupportedOperationException("not implemented")
|
||||
}
|
||||
|
||||
override fun moveMessagesAndMarkAsRead(sourceFolderServerId: String, targetFolderServerId: String, messageServerIds: List<String>): Map<String, String>? {
|
||||
throw UnsupportedOperationException("not implemented")
|
||||
}
|
||||
|
||||
override fun copyMessages(sourceFolderServerId: String, targetFolderServerId: String, messageServerIds: List<String>): Map<String, String>? {
|
||||
throw UnsupportedOperationException("not implemented")
|
||||
}
|
||||
|
||||
override fun search(folderServerId: String, query: String?, requiredFlags: Set<Flag>?, forbiddenFlags: Set<Flag>?): List<String> {
|
||||
throw UnsupportedOperationException("not implemented")
|
||||
}
|
||||
|
||||
override fun fetchMessage(folderServerId: String, messageServerId: String, fetchProfile: FetchProfile): Message {
|
||||
throw UnsupportedOperationException("not implemented")
|
||||
}
|
||||
|
||||
override fun fetchPart(folderServerId: String, messageServerId: String, part: Part, bodyFactory: BodyFactory) {
|
||||
throw UnsupportedOperationException("not implemented")
|
||||
}
|
||||
|
||||
override fun findByMessageId(folderServerId: String, messageId: String): String? {
|
||||
throw UnsupportedOperationException("not implemented")
|
||||
}
|
||||
|
||||
override fun uploadMessage(folderServerId: String, message: Message): String? {
|
||||
throw UnsupportedOperationException("not implemented")
|
||||
}
|
||||
|
||||
override fun createPusher(receiver: PushReceiver): Pusher {
|
||||
throw UnsupportedOperationException("not implemented")
|
||||
}
|
||||
|
||||
override fun checkIncomingServerSettings() {
|
||||
jmapClient.call(EchoMethodCall()).get()
|
||||
}
|
||||
|
||||
override fun sendMessage(message: Message) {
|
||||
throw UnsupportedOperationException("not implemented")
|
||||
}
|
||||
|
||||
override fun checkOutgoingServerSettings() {
|
||||
checkIncomingServerSettings()
|
||||
}
|
||||
|
||||
private fun JmapConfig.toJmapClient(): JmapClient {
|
||||
return if (baseUrl == null) {
|
||||
JmapClient(username, password)
|
||||
} else {
|
||||
val baseHttpUrl = HttpUrl.parse(baseUrl)
|
||||
JmapClient(username, password, baseHttpUrl)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
package com.fsck.k9.backend.jmap
|
||||
|
||||
data class JmapConfig(
|
||||
val username: String,
|
||||
val password: String,
|
||||
val baseUrl: String?,
|
||||
val accountId: String
|
||||
)
|
|
@ -0,0 +1,23 @@
|
|||
package com.fsck.k9.backend.jmap
|
||||
|
||||
import com.google.common.util.concurrent.ListenableFuture
|
||||
import java.util.concurrent.ExecutionException
|
||||
import rs.ltt.jmap.client.JmapRequest
|
||||
import rs.ltt.jmap.client.MethodResponses
|
||||
import rs.ltt.jmap.common.method.MethodResponse
|
||||
|
||||
internal inline fun <reified T : MethodResponse> ListenableFuture<MethodResponses>.getMainResponseBlocking(): T {
|
||||
return futureGetOrThrow().getMain(T::class.java)
|
||||
}
|
||||
|
||||
internal inline fun <reified T : MethodResponse> JmapRequest.Call.getMainResponseBlocking(): T {
|
||||
return methodResponses.getMainResponseBlocking()
|
||||
}
|
||||
|
||||
internal inline fun <T> ListenableFuture<T>.futureGetOrThrow(): T {
|
||||
return try {
|
||||
get()
|
||||
} catch (e: ExecutionException) {
|
||||
throw e.cause ?: e
|
||||
}
|
||||
}
|
|
@ -0,0 +1,195 @@
|
|||
package com.fsck.k9.backend.jmap
|
||||
|
||||
import com.fsck.k9.backend.api.FolderInfo
|
||||
import com.fsck.k9.mail.AuthenticationFailedException
|
||||
import com.fsck.k9.mail.FolderType
|
||||
import com.fsck.k9.mail.MessagingException
|
||||
import junit.framework.AssertionFailedError
|
||||
import okhttp3.HttpUrl
|
||||
import okhttp3.mockwebserver.MockResponse
|
||||
import okhttp3.mockwebserver.MockWebServer
|
||||
import okio.buffer
|
||||
import okio.source
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Assert.fail
|
||||
import org.junit.Test
|
||||
import rs.ltt.jmap.client.JmapClient
|
||||
|
||||
class CommandRefreshFolderListTest {
|
||||
private val backendStorage = InMemoryBackendStorage()
|
||||
|
||||
@Test
|
||||
fun sessionResourceWithAuthenticationError() {
|
||||
val command = createCommandRefreshFolderList(
|
||||
MockResponse().setResponseCode(401)
|
||||
)
|
||||
|
||||
try {
|
||||
command.refreshFolderList()
|
||||
fail("Expected exception")
|
||||
} catch (e: AuthenticationFailedException) {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun invalidSessionResource() {
|
||||
val command = createCommandRefreshFolderList(
|
||||
MockResponse().setBody("invalid")
|
||||
)
|
||||
|
||||
try {
|
||||
command.refreshFolderList()
|
||||
fail("Expected exception")
|
||||
} catch (e: MessagingException) {
|
||||
assertTrue(e.isPermanentFailure)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun fetchMailboxes() {
|
||||
val command = createCommandRefreshFolderList(
|
||||
responseBodyFromResource("/jmap_responses/session/valid_session.json"),
|
||||
responseBodyFromResource("/jmap_responses/mailbox/mailbox_get.json")
|
||||
)
|
||||
|
||||
command.refreshFolderList()
|
||||
|
||||
assertFolderList("id_inbox", "id_archive", "id_drafts", "id_sent", "id_trash", "id_folder1")
|
||||
assertFolderPresent("id_inbox", "Inbox", FolderType.INBOX)
|
||||
assertFolderPresent("id_archive", "Archive", FolderType.ARCHIVE)
|
||||
assertFolderPresent("id_drafts", "Drafts", FolderType.DRAFTS)
|
||||
assertFolderPresent("id_sent", "Sent", FolderType.SENT)
|
||||
assertFolderPresent("id_trash", "Trash", FolderType.TRASH)
|
||||
assertFolderPresent("id_folder1", "folder1", FolderType.REGULAR)
|
||||
assertMailboxState("23")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun fetchMailboxUpdates() {
|
||||
val command = createCommandRefreshFolderList(
|
||||
responseBodyFromResource("/jmap_responses/session/valid_session.json"),
|
||||
responseBodyFromResource("/jmap_responses/mailbox/mailbox_changes.json")
|
||||
)
|
||||
createFoldersInBackendStorage(state = "23")
|
||||
|
||||
command.refreshFolderList()
|
||||
|
||||
assertFolderList("id_inbox", "id_archive", "id_drafts", "id_sent", "id_trash", "id_folder2")
|
||||
assertFolderPresent("id_inbox", "Inbox", FolderType.INBOX)
|
||||
assertFolderPresent("id_archive", "Archive", FolderType.ARCHIVE)
|
||||
assertFolderPresent("id_drafts", "Drafts", FolderType.DRAFTS)
|
||||
assertFolderPresent("id_sent", "Sent", FolderType.SENT)
|
||||
assertFolderPresent("id_trash", "Deleted messages", FolderType.TRASH)
|
||||
assertFolderPresent("id_folder2", "folder2", FolderType.REGULAR)
|
||||
assertMailboxState("42")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun fetchMailboxUpdates_withHasMoreChanges() {
|
||||
val command = createCommandRefreshFolderList(
|
||||
responseBodyFromResource("/jmap_responses/session/valid_session.json"),
|
||||
responseBodyFromResource("/jmap_responses/mailbox/mailbox_changes_1.json"),
|
||||
responseBodyFromResource("/jmap_responses/mailbox/mailbox_changes_2.json")
|
||||
)
|
||||
createFoldersInBackendStorage(state = "23")
|
||||
|
||||
command.refreshFolderList()
|
||||
|
||||
assertFolderList("id_inbox", "id_archive", "id_drafts", "id_sent", "id_trash", "id_folder2")
|
||||
assertFolderPresent("id_inbox", "Inbox", FolderType.INBOX)
|
||||
assertFolderPresent("id_archive", "Archive", FolderType.ARCHIVE)
|
||||
assertFolderPresent("id_drafts", "Drafts", FolderType.DRAFTS)
|
||||
assertFolderPresent("id_sent", "Sent", FolderType.SENT)
|
||||
assertFolderPresent("id_trash", "Deleted messages", FolderType.TRASH)
|
||||
assertFolderPresent("id_folder2", "folder2", FolderType.REGULAR)
|
||||
assertMailboxState("42")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun fetchMailboxUpdates_withCannotCalculateChangesError() {
|
||||
val command = createCommandRefreshFolderList(
|
||||
responseBodyFromResource("/jmap_responses/session/valid_session.json"),
|
||||
responseBodyFromResource("/jmap_responses/mailbox/mailbox_changes_error_cannot_calculate_changes.json"),
|
||||
responseBodyFromResource("/jmap_responses/mailbox/mailbox_get.json")
|
||||
)
|
||||
setMailboxState("unknownToServer")
|
||||
|
||||
command.refreshFolderList()
|
||||
|
||||
assertFolderList("id_inbox", "id_archive", "id_drafts", "id_sent", "id_trash", "id_folder1")
|
||||
assertFolderPresent("id_inbox", "Inbox", FolderType.INBOX)
|
||||
assertFolderPresent("id_archive", "Archive", FolderType.ARCHIVE)
|
||||
assertFolderPresent("id_drafts", "Drafts", FolderType.DRAFTS)
|
||||
assertFolderPresent("id_sent", "Sent", FolderType.SENT)
|
||||
assertFolderPresent("id_trash", "Trash", FolderType.TRASH)
|
||||
assertFolderPresent("id_folder1", "folder1", FolderType.REGULAR)
|
||||
assertMailboxState("23")
|
||||
}
|
||||
|
||||
private fun createCommandRefreshFolderList(vararg mockResponses: MockResponse): CommandRefreshFolderList {
|
||||
val server = createMockWebServer(*mockResponses)
|
||||
return createCommandRefreshFolderList(server.url("/jmap/"))
|
||||
}
|
||||
|
||||
private fun createMockWebServer(vararg mockResponses: MockResponse): MockWebServer {
|
||||
return MockWebServer().apply {
|
||||
for (mockResponse in mockResponses) {
|
||||
enqueue(mockResponse)
|
||||
}
|
||||
start()
|
||||
}
|
||||
}
|
||||
|
||||
private fun createCommandRefreshFolderList(
|
||||
baseUrl: HttpUrl,
|
||||
accountId: String = "test@example.com"
|
||||
): CommandRefreshFolderList {
|
||||
val jmapClient = JmapClient("test", "test", baseUrl)
|
||||
return CommandRefreshFolderList(backendStorage, jmapClient, accountId)
|
||||
}
|
||||
|
||||
private fun responseBodyFromResource(name: String): MockResponse {
|
||||
return MockResponse().setBody(loadResource(name))
|
||||
}
|
||||
|
||||
private fun loadResource(name: String): String {
|
||||
val resourceAsStream = javaClass.getResourceAsStream(name) ?: error("Couldn't load resource: $name")
|
||||
return resourceAsStream.use { it.source().buffer().readUtf8() }
|
||||
}
|
||||
|
||||
@Suppress("SameParameterValue")
|
||||
private fun createFoldersInBackendStorage(state: String) {
|
||||
createFolderInBackendStorage("id_inbox", "Inbox", FolderType.INBOX)
|
||||
createFolderInBackendStorage("id_archive", "Archive", FolderType.ARCHIVE)
|
||||
createFolderInBackendStorage("id_drafts", "Drafts", FolderType.DRAFTS)
|
||||
createFolderInBackendStorage("id_sent", "Sent", FolderType.SENT)
|
||||
createFolderInBackendStorage("id_trash", "Trash", FolderType.TRASH)
|
||||
createFolderInBackendStorage("id_folder1", "folder1", FolderType.REGULAR)
|
||||
setMailboxState(state)
|
||||
}
|
||||
|
||||
private fun createFolderInBackendStorage(serverId: String, name: String, type: FolderType) {
|
||||
backendStorage.createFolders(listOf(FolderInfo(serverId, name, type)))
|
||||
}
|
||||
|
||||
private fun setMailboxState(state: String) {
|
||||
backendStorage.setExtraString("jmapState", state)
|
||||
}
|
||||
|
||||
private fun assertFolderList(vararg folderServerIds: String) {
|
||||
assertEquals(folderServerIds.toSet(), backendStorage.getFolderServerIds().toSet())
|
||||
}
|
||||
|
||||
private fun assertFolderPresent(serverId: String, name: String, type: FolderType) {
|
||||
val folder = backendStorage.folders[serverId]
|
||||
?: throw AssertionFailedError("Expected folder '$serverId' in BackendStorage")
|
||||
|
||||
assertEquals(name, folder.name)
|
||||
assertEquals(type, folder.type)
|
||||
}
|
||||
|
||||
private fun assertMailboxState(expected: String) {
|
||||
assertEquals(expected, backendStorage.getExtraString("jmapState"))
|
||||
}
|
||||
}
|
|
@ -0,0 +1,98 @@
|
|||
package com.fsck.k9.backend.jmap
|
||||
|
||||
import com.fsck.k9.backend.api.BackendFolder
|
||||
import com.fsck.k9.mail.Flag
|
||||
import com.fsck.k9.mail.FolderType
|
||||
import com.fsck.k9.mail.Message
|
||||
import java.util.Date
|
||||
|
||||
class InMemoryBackendFolder(override var name: String, var type: FolderType) : BackendFolder {
|
||||
val extraStrings: MutableMap<String, String> = mutableMapOf()
|
||||
val extraNumbers: MutableMap<String, Long> = mutableMapOf()
|
||||
|
||||
override var visibleLimit: Int = 25
|
||||
|
||||
override fun getAllMessagesAndEffectiveDates(): Map<String, Long?> {
|
||||
throw UnsupportedOperationException("not implemented")
|
||||
}
|
||||
|
||||
override fun destroyMessages(messageServerIds: List<String>) {
|
||||
throw UnsupportedOperationException("not implemented")
|
||||
}
|
||||
|
||||
override fun getLastUid(): Long? {
|
||||
throw UnsupportedOperationException("not implemented")
|
||||
}
|
||||
|
||||
override fun getMoreMessages(): BackendFolder.MoreMessages {
|
||||
throw UnsupportedOperationException("not implemented")
|
||||
}
|
||||
|
||||
override fun setMoreMessages(moreMessages: BackendFolder.MoreMessages) {
|
||||
throw UnsupportedOperationException("not implemented")
|
||||
}
|
||||
|
||||
override fun getUnreadMessageCount(): Int {
|
||||
throw UnsupportedOperationException("not implemented")
|
||||
}
|
||||
|
||||
override fun setLastChecked(timestamp: Long) {
|
||||
throw UnsupportedOperationException("not implemented")
|
||||
}
|
||||
|
||||
override fun setStatus(status: String?) {
|
||||
throw UnsupportedOperationException("not implemented")
|
||||
}
|
||||
|
||||
override fun getPushState(): String? {
|
||||
throw UnsupportedOperationException("not implemented")
|
||||
}
|
||||
|
||||
override fun setPushState(pushState: String?) {
|
||||
throw UnsupportedOperationException("not implemented")
|
||||
}
|
||||
|
||||
override fun isMessagePresent(messageServerId: String): Boolean {
|
||||
throw UnsupportedOperationException("not implemented")
|
||||
}
|
||||
|
||||
override fun getMessageFlags(messageServerId: String): Set<Flag> {
|
||||
throw UnsupportedOperationException("not implemented")
|
||||
}
|
||||
|
||||
override fun setMessageFlag(messageServerId: String, flag: Flag, value: Boolean) {
|
||||
throw UnsupportedOperationException("not implemented")
|
||||
}
|
||||
|
||||
override fun savePartialMessage(message: Message) {
|
||||
throw UnsupportedOperationException("not implemented")
|
||||
}
|
||||
|
||||
override fun saveCompleteMessage(message: Message) {
|
||||
throw UnsupportedOperationException("not implemented")
|
||||
}
|
||||
|
||||
override fun getLatestOldMessageSeenTime(): Date {
|
||||
throw UnsupportedOperationException("not implemented")
|
||||
}
|
||||
|
||||
override fun setLatestOldMessageSeenTime(date: Date) {
|
||||
throw UnsupportedOperationException("not implemented")
|
||||
}
|
||||
|
||||
override fun getOldestMessageDate(): Date? {
|
||||
throw UnsupportedOperationException("not implemented")
|
||||
}
|
||||
|
||||
override fun getFolderExtraString(name: String): String? = extraStrings[name]
|
||||
|
||||
override fun setFolderExtraString(name: String, value: String) {
|
||||
extraStrings[name] = value
|
||||
}
|
||||
|
||||
override fun getFolderExtraNumber(name: String): Long? = extraNumbers[name]
|
||||
|
||||
override fun setFolderExtraNumber(name: String, value: Long) {
|
||||
extraNumbers[name] = value
|
||||
}
|
||||
}
|
|
@ -0,0 +1,52 @@
|
|||
package com.fsck.k9.backend.jmap
|
||||
|
||||
import com.fsck.k9.backend.api.BackendFolder
|
||||
import com.fsck.k9.backend.api.BackendStorage
|
||||
import com.fsck.k9.backend.api.FolderInfo
|
||||
import com.fsck.k9.mail.FolderType
|
||||
|
||||
class InMemoryBackendStorage : BackendStorage {
|
||||
val folders: MutableMap<String, InMemoryBackendFolder> = mutableMapOf()
|
||||
val extraStrings: MutableMap<String, String> = mutableMapOf()
|
||||
val extraNumbers: MutableMap<String, Long> = mutableMapOf()
|
||||
|
||||
override fun getFolder(folderServerId: String): BackendFolder {
|
||||
return folders[folderServerId] ?: error("Folder $folderServerId not found")
|
||||
}
|
||||
|
||||
override fun getFolderServerIds(): List<String> {
|
||||
return folders.keys.toList()
|
||||
}
|
||||
|
||||
override fun createFolders(folders: List<FolderInfo>) {
|
||||
folders.forEach { folder ->
|
||||
if (this.folders.containsKey(folder.serverId)) error("Folder ${folder.serverId} already present")
|
||||
|
||||
this.folders[folder.serverId] = InMemoryBackendFolder(folder.name, folder.type)
|
||||
}
|
||||
}
|
||||
|
||||
override fun deleteFolders(folderServerIds: List<String>) {
|
||||
for (folderServerId in folderServerIds) {
|
||||
folders.remove(folderServerId) ?: error("Folder $folderServerId not found")
|
||||
}
|
||||
}
|
||||
|
||||
override fun changeFolder(folderServerId: String, name: String, type: FolderType) {
|
||||
val folder = folders[folderServerId] ?: error("Folder $folderServerId not found")
|
||||
folder.name = name
|
||||
folder.type = type
|
||||
}
|
||||
|
||||
override fun getExtraString(name: String): String? = extraStrings[name]
|
||||
|
||||
override fun setExtraString(name: String, value: String) {
|
||||
extraStrings[name] = value
|
||||
}
|
||||
|
||||
override fun getExtraNumber(name: String): Long? = extraNumbers[name]
|
||||
|
||||
override fun setExtraNumber(name: String, value: Long) {
|
||||
extraNumbers[name] = value
|
||||
}
|
||||
}
|
|
@ -0,0 +1,86 @@
|
|||
{
|
||||
"methodResponses": [
|
||||
[
|
||||
"Mailbox/changes",
|
||||
{
|
||||
"accountId": "test@example.com",
|
||||
"oldState": "23",
|
||||
"newState": "42",
|
||||
"hasMoreChanges": false,
|
||||
"created": [ "id_folder2" ],
|
||||
"updated": [ "id_trash" ],
|
||||
"destroyed": [ "id_folder1" ]
|
||||
},
|
||||
"0"
|
||||
],
|
||||
[
|
||||
"Mailbox/get",
|
||||
{
|
||||
"accountId": "test@example.com",
|
||||
"state": "42",
|
||||
"list": [
|
||||
{
|
||||
"id": "id_folder2",
|
||||
"name": "folder2",
|
||||
"parentId": null,
|
||||
"myRights": {
|
||||
"mayReadItems": true,
|
||||
"mayAddItems": true,
|
||||
"mayRemoveItems": true,
|
||||
"mayCreateChild": true,
|
||||
"mayDelete": true,
|
||||
"maySubmit": true,
|
||||
"maySetSeen": true,
|
||||
"maySetKeywords": true,
|
||||
"mayAdmin": true,
|
||||
"mayRename": true
|
||||
},
|
||||
"role": null,
|
||||
"totalEmails": 0,
|
||||
"unreadEmails": 0,
|
||||
"totalThreads": 0,
|
||||
"unreadThreads": 0,
|
||||
"sortOrder": 10,
|
||||
"isSubscribed": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"1"
|
||||
],
|
||||
[
|
||||
"Mailbox/get",
|
||||
{
|
||||
"accountId": "test@example.com",
|
||||
"state": "42",
|
||||
"list": [
|
||||
{
|
||||
"id": "id_trash",
|
||||
"name": "Deleted messages",
|
||||
"parentId": null,
|
||||
"myRights": {
|
||||
"mayReadItems": true,
|
||||
"mayAddItems": true,
|
||||
"mayRemoveItems": true,
|
||||
"mayCreateChild": true,
|
||||
"mayDelete": true,
|
||||
"maySubmit": true,
|
||||
"maySetSeen": true,
|
||||
"maySetKeywords": true,
|
||||
"mayAdmin": true,
|
||||
"mayRename": true
|
||||
},
|
||||
"role": "trash",
|
||||
"totalEmails": 2,
|
||||
"unreadEmails": 0,
|
||||
"totalThreads": 2,
|
||||
"unreadThreads": 0,
|
||||
"sortOrder": 7,
|
||||
"isSubscribed": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"2"
|
||||
]
|
||||
],
|
||||
"sessionState": "0"
|
||||
}
|
|
@ -0,0 +1,61 @@
|
|||
{
|
||||
"methodResponses": [
|
||||
[
|
||||
"Mailbox/changes",
|
||||
{
|
||||
"accountId": "test@example.com",
|
||||
"oldState": "23",
|
||||
"newState": "27",
|
||||
"hasMoreChanges": true,
|
||||
"created": [ "id_folder2" ],
|
||||
"updated": [],
|
||||
"destroyed": []
|
||||
},
|
||||
"0"
|
||||
],
|
||||
[
|
||||
"Mailbox/get",
|
||||
{
|
||||
"accountId": "test@example.com",
|
||||
"state": "27",
|
||||
"list": [
|
||||
{
|
||||
"id": "id_folder2",
|
||||
"name": "folder2",
|
||||
"parentId": null,
|
||||
"myRights": {
|
||||
"mayReadItems": true,
|
||||
"mayAddItems": true,
|
||||
"mayRemoveItems": true,
|
||||
"mayCreateChild": true,
|
||||
"mayDelete": true,
|
||||
"maySubmit": true,
|
||||
"maySetSeen": true,
|
||||
"maySetKeywords": true,
|
||||
"mayAdmin": true,
|
||||
"mayRename": true
|
||||
},
|
||||
"role": null,
|
||||
"totalEmails": 0,
|
||||
"unreadEmails": 0,
|
||||
"totalThreads": 0,
|
||||
"unreadThreads": 0,
|
||||
"sortOrder": 10,
|
||||
"isSubscribed": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"1"
|
||||
],
|
||||
[
|
||||
"Mailbox/get",
|
||||
{
|
||||
"accountId": "test@example.com",
|
||||
"state": "27",
|
||||
"list": []
|
||||
},
|
||||
"2"
|
||||
]
|
||||
],
|
||||
"sessionState": "0"
|
||||
}
|
|
@ -0,0 +1,61 @@
|
|||
{
|
||||
"methodResponses": [
|
||||
[
|
||||
"Mailbox/changes",
|
||||
{
|
||||
"accountId": "test@example.com",
|
||||
"oldState": "27",
|
||||
"newState": "42",
|
||||
"hasMoreChanges": false,
|
||||
"created": [],
|
||||
"updated": [ "id_trash" ],
|
||||
"destroyed": [ "id_folder1" ]
|
||||
},
|
||||
"0"
|
||||
],
|
||||
[
|
||||
"Mailbox/get",
|
||||
{
|
||||
"accountId": "test@example.com",
|
||||
"state": "42",
|
||||
"list": []
|
||||
},
|
||||
"1"
|
||||
],
|
||||
[
|
||||
"Mailbox/get",
|
||||
{
|
||||
"accountId": "test@example.com",
|
||||
"state": "42",
|
||||
"list": [
|
||||
{
|
||||
"id": "id_trash",
|
||||
"name": "Deleted messages",
|
||||
"parentId": null,
|
||||
"myRights": {
|
||||
"mayReadItems": true,
|
||||
"mayAddItems": true,
|
||||
"mayRemoveItems": true,
|
||||
"mayCreateChild": true,
|
||||
"mayDelete": true,
|
||||
"maySubmit": true,
|
||||
"maySetSeen": true,
|
||||
"maySetKeywords": true,
|
||||
"mayAdmin": true,
|
||||
"mayRename": true
|
||||
},
|
||||
"role": "trash",
|
||||
"totalEmails": 2,
|
||||
"unreadEmails": 0,
|
||||
"totalThreads": 2,
|
||||
"unreadThreads": 0,
|
||||
"sortOrder": 7,
|
||||
"isSubscribed": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"2"
|
||||
]
|
||||
],
|
||||
"sessionState": "0"
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
{
|
||||
"methodResponses": [
|
||||
[
|
||||
"error",
|
||||
{
|
||||
"type": "cannotCalculateChanges"
|
||||
},
|
||||
"0"
|
||||
],
|
||||
[
|
||||
"error",
|
||||
{
|
||||
"type": "resultReference"
|
||||
},
|
||||
"1"
|
||||
],
|
||||
[
|
||||
"error",
|
||||
{
|
||||
"type": "resultReference"
|
||||
},
|
||||
"2"
|
||||
]
|
||||
],
|
||||
"sessionState": "0"
|
||||
}
|
|
@ -0,0 +1,159 @@
|
|||
{
|
||||
"methodResponses": [
|
||||
[
|
||||
"Mailbox/get",
|
||||
{
|
||||
"accountId": "test@example.com",
|
||||
"state": "23",
|
||||
"list": [
|
||||
{
|
||||
"id": "id_inbox",
|
||||
"name": "Inbox",
|
||||
"parentId": null,
|
||||
"myRights": {
|
||||
"mayReadItems": true,
|
||||
"mayAddItems": true,
|
||||
"mayRemoveItems": true,
|
||||
"mayCreateChild": true,
|
||||
"mayDelete": false,
|
||||
"maySubmit": true,
|
||||
"maySetSeen": true,
|
||||
"maySetKeywords": true,
|
||||
"mayAdmin": true,
|
||||
"mayRename": false
|
||||
},
|
||||
"role": "inbox",
|
||||
"totalEmails": 238,
|
||||
"unreadEmails": 6,
|
||||
"totalThreads": 80,
|
||||
"unreadThreads": 4,
|
||||
"sortOrder": 1,
|
||||
"isSubscribed": false
|
||||
},
|
||||
{
|
||||
"id": "id_archive",
|
||||
"name": "Archive",
|
||||
"parentId": null,
|
||||
"myRights": {
|
||||
"mayReadItems": true,
|
||||
"mayAddItems": true,
|
||||
"mayRemoveItems": true,
|
||||
"mayCreateChild": true,
|
||||
"mayDelete": true,
|
||||
"maySubmit": true,
|
||||
"maySetSeen": true,
|
||||
"maySetKeywords": true,
|
||||
"mayAdmin": true,
|
||||
"mayRename": true
|
||||
},
|
||||
"role": "archive",
|
||||
"totalEmails": 295,
|
||||
"unreadEmails": 36,
|
||||
"totalThreads": 136,
|
||||
"unreadThreads": 17,
|
||||
"sortOrder": 3,
|
||||
"isSubscribed": false
|
||||
},
|
||||
{
|
||||
"id": "id_drafts",
|
||||
"name": "Drafts",
|
||||
"parentId": null,
|
||||
"myRights": {
|
||||
"mayReadItems": true,
|
||||
"mayAddItems": true,
|
||||
"mayRemoveItems": true,
|
||||
"mayCreateChild": true,
|
||||
"mayDelete": true,
|
||||
"maySubmit": true,
|
||||
"maySetSeen": true,
|
||||
"maySetKeywords": true,
|
||||
"mayAdmin": true,
|
||||
"mayRename": true
|
||||
},
|
||||
"role": "drafts",
|
||||
"totalEmails": 0,
|
||||
"unreadEmails": 0,
|
||||
"totalThreads": 0,
|
||||
"unreadThreads": 0,
|
||||
"sortOrder": 4,
|
||||
"isSubscribed": false
|
||||
},
|
||||
{
|
||||
"id": "id_sent",
|
||||
"name": "Sent",
|
||||
"parentId": null,
|
||||
"myRights": {
|
||||
"mayReadItems": true,
|
||||
"mayAddItems": true,
|
||||
"mayRemoveItems": true,
|
||||
"mayCreateChild": true,
|
||||
"mayDelete": true,
|
||||
"maySubmit": true,
|
||||
"maySetSeen": true,
|
||||
"maySetKeywords": true,
|
||||
"mayAdmin": true,
|
||||
"mayRename": true
|
||||
},
|
||||
"role": "sent",
|
||||
"totalEmails": 2,
|
||||
"unreadEmails": 0,
|
||||
"totalThreads": 2,
|
||||
"unreadThreads": 0,
|
||||
"sortOrder": 5,
|
||||
"isSubscribed": false
|
||||
},
|
||||
{
|
||||
"id": "id_trash",
|
||||
"name": "Trash",
|
||||
"parentId": null,
|
||||
"myRights": {
|
||||
"mayReadItems": true,
|
||||
"mayAddItems": true,
|
||||
"mayRemoveItems": true,
|
||||
"mayCreateChild": true,
|
||||
"mayDelete": true,
|
||||
"maySubmit": true,
|
||||
"maySetSeen": true,
|
||||
"maySetKeywords": true,
|
||||
"mayAdmin": true,
|
||||
"mayRename": true
|
||||
},
|
||||
"role": "trash",
|
||||
"totalEmails": 2,
|
||||
"unreadEmails": 0,
|
||||
"totalThreads": 2,
|
||||
"unreadThreads": 0,
|
||||
"sortOrder": 7,
|
||||
"isSubscribed": false
|
||||
},
|
||||
{
|
||||
"id": "id_folder1",
|
||||
"name": "folder1",
|
||||
"parentId": null,
|
||||
"myRights": {
|
||||
"mayReadItems": true,
|
||||
"mayAddItems": true,
|
||||
"mayRemoveItems": true,
|
||||
"mayCreateChild": true,
|
||||
"mayDelete": true,
|
||||
"maySubmit": true,
|
||||
"maySetSeen": true,
|
||||
"maySetKeywords": true,
|
||||
"mayAdmin": true,
|
||||
"mayRename": true
|
||||
},
|
||||
"role": null,
|
||||
"totalEmails": 0,
|
||||
"unreadEmails": 0,
|
||||
"totalThreads": 0,
|
||||
"unreadThreads": 0,
|
||||
"sortOrder": 10,
|
||||
"isSubscribed": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"0"
|
||||
]
|
||||
],
|
||||
"sessionState": "0"
|
||||
}
|
|
@ -0,0 +1,64 @@
|
|||
{
|
||||
"username": "test",
|
||||
"apiUrl": "/jmap/",
|
||||
"downloadUrl": "/jmap/download/{accountId}/{blobId}/{name}?accept={type}",
|
||||
"uploadUrl": "/jmap/upload/{accountId}/",
|
||||
"eventSourceUrl": "/jmap/eventsource/?types={types}&closeafter={closeafter}&ping={ping}",
|
||||
"accounts": {
|
||||
"test@example.com": {
|
||||
"name": "test@example.com",
|
||||
"isPersonal": true,
|
||||
"isReadOnly": false,
|
||||
"accountCapabilities": {
|
||||
"urn:ietf:params:jmap:core": {},
|
||||
"urn:ietf:params:jmap:submission": {
|
||||
"maxDelayedSend": 44236800,
|
||||
"submissionExtensions": {
|
||||
"size": [
|
||||
"10240000"
|
||||
],
|
||||
"dsn": []
|
||||
}
|
||||
},
|
||||
"urn:ietf:params:jmap:mail": {
|
||||
"emailQuerySortOptions": [
|
||||
"receivedAt",
|
||||
"sentAt",
|
||||
"from",
|
||||
"id",
|
||||
"emailstate",
|
||||
"size",
|
||||
"subject",
|
||||
"to",
|
||||
"hasKeyword",
|
||||
"someInThreadHaveKeyword",
|
||||
"addedDates",
|
||||
"threadSize",
|
||||
"spamScore",
|
||||
"snoozedUntil"
|
||||
],
|
||||
"maxKeywordsPerEmail": 100,
|
||||
"maxSizeAttachmentsPerEmail": 10485760,
|
||||
"maxMailboxesPerEmail": 20,
|
||||
"mayCreateTopLevelMailbox": true,
|
||||
"maxSizeMailboxName": 500
|
||||
},
|
||||
"urn:ietf:params:jmap:vacationresponse": {}
|
||||
}
|
||||
}
|
||||
},
|
||||
"capabilities": {
|
||||
"urn:ietf:params:jmap:core": {
|
||||
"maxSizeUpload": 1073741824,
|
||||
"maxConcurrentUpload": 5,
|
||||
"maxCallsInRequest": 50,
|
||||
"maxObjectsInGet": 4096,
|
||||
"maxObjectsInSet": 4096,
|
||||
"collationAlgorithms": []
|
||||
},
|
||||
"urn:ietf:params:jmap:submission": {},
|
||||
"urn:ietf:params:jmap:mail": {},
|
||||
"urn:ietf:params:jmap:vacationresponse": {}
|
||||
},
|
||||
"state": "0"
|
||||
}
|
|
@ -12,7 +12,7 @@ buildscript {
|
|||
'kotlin': '1.3.50',
|
||||
'androidxAppCompat': '1.0.2',
|
||||
'androidxRecyclerView': '1.0.0',
|
||||
'androidxLifecycleExtensions': '2.0.0',
|
||||
'androidxLifecycleExtensions': '2.1.0',
|
||||
'androidxAnnotation': '1.0.1',
|
||||
'androidxNavigation': '2.0.0',
|
||||
'androidxConstraintLayout': '1.1.3',
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
android.useAndroidX=true
|
||||
android.enableJetifier=true
|
||||
org.gradle.jvmargs=-Xmx1g
|
||||
org.gradle.jvmargs=-Xmx1400m
|
||||
org.gradle.parallel=true
|
||||
|
|
|
@ -6,6 +6,10 @@ public class MessagingException extends Exception {
|
|||
|
||||
private boolean permanentFailure = false;
|
||||
|
||||
public MessagingException(Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
|
||||
public MessagingException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
include ':app:k9mail'
|
||||
include ':app:k9mail-jmap'
|
||||
include ':app:ui'
|
||||
include ':app:core'
|
||||
include ':app:storage'
|
||||
|
@ -15,4 +16,5 @@ include ':backend:api'
|
|||
include ':backend:imap'
|
||||
include ':backend:pop3'
|
||||
include ':backend:webdav'
|
||||
include ':backend:jmap'
|
||||
include ':plugins:openpgp-api-lib:openpgp-api'
|
||||
|
|
Loading…
Reference in a new issue