Repo created
This commit is contained in:
parent
a629de6271
commit
3cef7c5092
2161 changed files with 246605 additions and 2 deletions
51
app/core/build.gradle.kts
Normal file
51
app/core/build.gradle.kts
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
@Suppress("DSL_SCOPE_VIOLATION")
|
||||
plugins {
|
||||
id(ThunderbirdPlugins.Library.android)
|
||||
alias(libs.plugins.kotlin.parcelize)
|
||||
}
|
||||
|
||||
dependencies {
|
||||
api(projects.mail.common)
|
||||
api(projects.backend.api)
|
||||
api(projects.app.htmlCleaner)
|
||||
api(projects.core.android.common)
|
||||
|
||||
implementation(projects.plugins.openpgpApiLib.openpgpApi)
|
||||
|
||||
api(libs.koin.android)
|
||||
|
||||
api(libs.androidx.annotation)
|
||||
|
||||
implementation(libs.okio)
|
||||
implementation(libs.commons.io)
|
||||
implementation(libs.androidx.core.ktx)
|
||||
implementation(libs.androidx.work.ktx)
|
||||
implementation(libs.androidx.fragment)
|
||||
implementation(libs.androidx.localbroadcastmanager)
|
||||
implementation(libs.jsoup)
|
||||
implementation(libs.moshi)
|
||||
implementation(libs.timber)
|
||||
implementation(libs.mime4j.core)
|
||||
implementation(libs.mime4j.dom)
|
||||
|
||||
testApi(projects.core.testing)
|
||||
testImplementation(projects.mail.testing)
|
||||
testImplementation(projects.backend.imap)
|
||||
testImplementation(projects.mail.protocols.smtp)
|
||||
testImplementation(projects.app.storage)
|
||||
testImplementation(projects.app.testing)
|
||||
|
||||
testImplementation(libs.kotlin.test)
|
||||
testImplementation(libs.kotlin.reflect)
|
||||
testImplementation(libs.robolectric)
|
||||
testImplementation(libs.androidx.test.core)
|
||||
testImplementation(libs.jdom2)
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.fsck.k9.core"
|
||||
|
||||
buildFeatures {
|
||||
buildConfig = true
|
||||
}
|
||||
}
|
||||
7
app/core/src/main/AndroidManifest.xml
Normal file
7
app/core/src/main/AndroidManifest.xml
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
|
||||
<uses-permission android:name="android.permission.READ_SYNC_SETTINGS" />
|
||||
|
||||
</manifest>
|
||||
696
app/core/src/main/java/com/fsck/k9/Account.kt
Normal file
696
app/core/src/main/java/com/fsck/k9/Account.kt
Normal file
|
|
@ -0,0 +1,696 @@
|
|||
package com.fsck.k9
|
||||
|
||||
import com.fsck.k9.backend.api.SyncConfig.ExpungePolicy
|
||||
import com.fsck.k9.mail.Address
|
||||
import com.fsck.k9.mail.ServerSettings
|
||||
import java.util.Calendar
|
||||
import java.util.Date
|
||||
|
||||
/**
|
||||
* Account stores all of the settings for a single account defined by the user. Each account is defined by a UUID.
|
||||
*/
|
||||
class Account(override val uuid: String) : BaseAccount {
|
||||
@get:Synchronized
|
||||
@set:Synchronized
|
||||
var deletePolicy = DeletePolicy.NEVER
|
||||
|
||||
@get:Synchronized
|
||||
@set:Synchronized
|
||||
private var internalIncomingServerSettings: ServerSettings? = null
|
||||
|
||||
@get:Synchronized
|
||||
@set:Synchronized
|
||||
private var internalOutgoingServerSettings: ServerSettings? = null
|
||||
|
||||
var incomingServerSettings: ServerSettings
|
||||
get() = internalIncomingServerSettings ?: error("Incoming server settings not set yet")
|
||||
set(value) {
|
||||
internalIncomingServerSettings = value
|
||||
}
|
||||
|
||||
var outgoingServerSettings: ServerSettings
|
||||
get() = internalOutgoingServerSettings ?: error("Outgoing server settings not set yet")
|
||||
set(value) {
|
||||
internalOutgoingServerSettings = value
|
||||
}
|
||||
|
||||
@get:Synchronized
|
||||
@set:Synchronized
|
||||
var oAuthState: String? = null
|
||||
|
||||
/**
|
||||
* Storage provider ID, used to locate and manage the underlying DB/file storage.
|
||||
*/
|
||||
@get:Synchronized
|
||||
@set:Synchronized
|
||||
var localStorageProviderId: String? = null
|
||||
|
||||
@get:Synchronized
|
||||
@set:Synchronized
|
||||
override var name: String? = null
|
||||
set(value) {
|
||||
field = value?.takeIf { it.isNotEmpty() }
|
||||
}
|
||||
|
||||
@get:Synchronized
|
||||
@set:Synchronized
|
||||
var alwaysBcc: String? = null
|
||||
|
||||
/**
|
||||
* -1 for never.
|
||||
*/
|
||||
@get:Synchronized
|
||||
@set:Synchronized
|
||||
var automaticCheckIntervalMinutes = 0
|
||||
|
||||
@get:Synchronized
|
||||
@set:Synchronized
|
||||
var displayCount = 0
|
||||
set(value) {
|
||||
if (field != value) {
|
||||
field = value.takeIf { it != -1 } ?: K9.DEFAULT_VISIBLE_LIMIT
|
||||
isChangedVisibleLimits = true
|
||||
}
|
||||
}
|
||||
|
||||
@get:Synchronized
|
||||
@set:Synchronized
|
||||
var chipColor = 0
|
||||
|
||||
@get:Synchronized
|
||||
@set:Synchronized
|
||||
var isNotifyNewMail = false
|
||||
|
||||
@get:Synchronized
|
||||
@set:Synchronized
|
||||
var folderNotifyNewMailMode = FolderMode.ALL
|
||||
|
||||
@get:Synchronized
|
||||
@set:Synchronized
|
||||
var isNotifySelfNewMail = false
|
||||
|
||||
@get:Synchronized
|
||||
@set:Synchronized
|
||||
var isNotifyContactsMailOnly = false
|
||||
|
||||
@get:Synchronized
|
||||
@set:Synchronized
|
||||
var isIgnoreChatMessages = false
|
||||
|
||||
@get:Synchronized
|
||||
@set:Synchronized
|
||||
var legacyInboxFolder: String? = null
|
||||
|
||||
@get:Synchronized
|
||||
@set:Synchronized
|
||||
var importedDraftsFolder: String? = null
|
||||
|
||||
@get:Synchronized
|
||||
@set:Synchronized
|
||||
var importedSentFolder: String? = null
|
||||
|
||||
@get:Synchronized
|
||||
@set:Synchronized
|
||||
var importedTrashFolder: String? = null
|
||||
|
||||
@get:Synchronized
|
||||
@set:Synchronized
|
||||
var importedArchiveFolder: String? = null
|
||||
|
||||
@get:Synchronized
|
||||
@set:Synchronized
|
||||
var importedSpamFolder: String? = null
|
||||
|
||||
@get:Synchronized
|
||||
@set:Synchronized
|
||||
var inboxFolderId: Long? = null
|
||||
|
||||
@get:Synchronized
|
||||
@set:Synchronized
|
||||
var outboxFolderId: Long? = null
|
||||
|
||||
@get:Synchronized
|
||||
@set:Synchronized
|
||||
var draftsFolderId: Long? = null
|
||||
|
||||
@get:Synchronized
|
||||
@set:Synchronized
|
||||
var sentFolderId: Long? = null
|
||||
|
||||
@get:Synchronized
|
||||
@set:Synchronized
|
||||
var trashFolderId: Long? = null
|
||||
|
||||
@get:Synchronized
|
||||
@set:Synchronized
|
||||
var archiveFolderId: Long? = null
|
||||
|
||||
@get:Synchronized
|
||||
@set:Synchronized
|
||||
var spamFolderId: Long? = null
|
||||
|
||||
@get:Synchronized
|
||||
var draftsFolderSelection = SpecialFolderSelection.AUTOMATIC
|
||||
private set
|
||||
|
||||
@get:Synchronized
|
||||
var sentFolderSelection = SpecialFolderSelection.AUTOMATIC
|
||||
private set
|
||||
|
||||
@get:Synchronized
|
||||
var trashFolderSelection = SpecialFolderSelection.AUTOMATIC
|
||||
private set
|
||||
|
||||
@get:Synchronized
|
||||
var archiveFolderSelection = SpecialFolderSelection.AUTOMATIC
|
||||
private set
|
||||
|
||||
@get:Synchronized
|
||||
var spamFolderSelection = SpecialFolderSelection.AUTOMATIC
|
||||
private set
|
||||
|
||||
@get:Synchronized
|
||||
@set:Synchronized
|
||||
var importedAutoExpandFolder: String? = null
|
||||
|
||||
@get:Synchronized
|
||||
@set:Synchronized
|
||||
var autoExpandFolderId: Long? = null
|
||||
|
||||
@get:Synchronized
|
||||
@set:Synchronized
|
||||
var folderDisplayMode = FolderMode.NOT_SECOND_CLASS
|
||||
|
||||
@get:Synchronized
|
||||
@set:Synchronized
|
||||
var folderSyncMode = FolderMode.FIRST_CLASS
|
||||
|
||||
@get:Synchronized
|
||||
@set:Synchronized
|
||||
var folderPushMode = FolderMode.NONE
|
||||
|
||||
@get:Synchronized
|
||||
@set:Synchronized
|
||||
var folderTargetMode = FolderMode.NOT_SECOND_CLASS
|
||||
|
||||
@get:Synchronized
|
||||
@set:Synchronized
|
||||
var accountNumber = 0
|
||||
|
||||
@get:Synchronized
|
||||
@set:Synchronized
|
||||
var isNotifySync = false
|
||||
|
||||
@get:Synchronized
|
||||
@set:Synchronized
|
||||
var sortType: SortType = SortType.SORT_DATE
|
||||
|
||||
private val sortAscending: MutableMap<SortType, Boolean> = mutableMapOf()
|
||||
|
||||
@get:Synchronized
|
||||
@set:Synchronized
|
||||
var showPictures = ShowPictures.NEVER
|
||||
|
||||
@get:Synchronized
|
||||
@set:Synchronized
|
||||
var isSignatureBeforeQuotedText = false
|
||||
|
||||
@get:Synchronized
|
||||
@set:Synchronized
|
||||
var expungePolicy = Expunge.EXPUNGE_IMMEDIATELY
|
||||
|
||||
@get:Synchronized
|
||||
@set:Synchronized
|
||||
var maxPushFolders = 0
|
||||
|
||||
@get:Synchronized
|
||||
@set:Synchronized
|
||||
var idleRefreshMinutes = 0
|
||||
|
||||
@get:JvmName("useCompression")
|
||||
@get:Synchronized
|
||||
@set:Synchronized
|
||||
var useCompression = true
|
||||
|
||||
@get:Synchronized
|
||||
@set:Synchronized
|
||||
var searchableFolders = Searchable.ALL
|
||||
|
||||
@get:Synchronized
|
||||
@set:Synchronized
|
||||
var isSubscribedFoldersOnly = false
|
||||
|
||||
@get:Synchronized
|
||||
@set:Synchronized
|
||||
var maximumPolledMessageAge = 0
|
||||
|
||||
@get:Synchronized
|
||||
@set:Synchronized
|
||||
var maximumAutoDownloadMessageSize = 0
|
||||
|
||||
@get:Synchronized
|
||||
@set:Synchronized
|
||||
var messageFormat = MessageFormat.HTML
|
||||
|
||||
@get:Synchronized
|
||||
@set:Synchronized
|
||||
var isMessageFormatAuto = false
|
||||
|
||||
@get:Synchronized
|
||||
@set:Synchronized
|
||||
var isMessageReadReceipt = false
|
||||
|
||||
@get:Synchronized
|
||||
@set:Synchronized
|
||||
var quoteStyle = QuoteStyle.PREFIX
|
||||
|
||||
@get:Synchronized
|
||||
@set:Synchronized
|
||||
var quotePrefix: String? = null
|
||||
|
||||
@get:Synchronized
|
||||
@set:Synchronized
|
||||
var isDefaultQuotedTextShown = false
|
||||
|
||||
@get:Synchronized
|
||||
@set:Synchronized
|
||||
var isReplyAfterQuote = false
|
||||
|
||||
@get:Synchronized
|
||||
@set:Synchronized
|
||||
var isStripSignature = false
|
||||
|
||||
@get:Synchronized
|
||||
@set:Synchronized
|
||||
var isSyncRemoteDeletions = false
|
||||
|
||||
@get:Synchronized
|
||||
@set:Synchronized
|
||||
var openPgpProvider: String? = null
|
||||
set(value) {
|
||||
field = value?.takeIf { it.isNotEmpty() }
|
||||
}
|
||||
|
||||
@get:Synchronized
|
||||
@set:Synchronized
|
||||
var openPgpKey: Long = 0
|
||||
|
||||
@get:Synchronized
|
||||
@set:Synchronized
|
||||
var autocryptPreferEncryptMutual = false
|
||||
|
||||
@get:Synchronized
|
||||
@set:Synchronized
|
||||
var isOpenPgpHideSignOnly = false
|
||||
|
||||
@get:Synchronized
|
||||
@set:Synchronized
|
||||
var isOpenPgpEncryptSubject = false
|
||||
|
||||
@get:Synchronized
|
||||
@set:Synchronized
|
||||
var isOpenPgpEncryptAllDrafts = false
|
||||
|
||||
@get:Synchronized
|
||||
@set:Synchronized
|
||||
var isMarkMessageAsReadOnView = false
|
||||
|
||||
@get:Synchronized
|
||||
@set:Synchronized
|
||||
var isMarkMessageAsReadOnDelete = false
|
||||
|
||||
@get:Synchronized
|
||||
@set:Synchronized
|
||||
var isAlwaysShowCcBcc = false
|
||||
|
||||
// Temporarily disabled
|
||||
@get:Synchronized
|
||||
@set:Synchronized
|
||||
var isRemoteSearchFullText = false
|
||||
get() = false
|
||||
|
||||
@get:Synchronized
|
||||
@set:Synchronized
|
||||
var remoteSearchNumResults = 0
|
||||
set(value) {
|
||||
field = value.coerceAtLeast(0)
|
||||
}
|
||||
|
||||
@get:Synchronized
|
||||
@set:Synchronized
|
||||
var isUploadSentMessages = false
|
||||
|
||||
@get:Synchronized
|
||||
@set:Synchronized
|
||||
var lastSyncTime: Long = 0
|
||||
|
||||
@get:Synchronized
|
||||
@set:Synchronized
|
||||
var lastFolderListRefreshTime: Long = 0
|
||||
|
||||
@get:Synchronized
|
||||
var isFinishedSetup = false
|
||||
private set
|
||||
|
||||
@get:Synchronized
|
||||
@set:Synchronized
|
||||
var messagesNotificationChannelVersion = 0
|
||||
|
||||
@get:Synchronized
|
||||
@set:Synchronized
|
||||
var isChangedVisibleLimits = false
|
||||
private set
|
||||
|
||||
/**
|
||||
* Database ID of the folder that was last selected for a copy or move operation.
|
||||
*
|
||||
* Note: For now this value isn't persisted. So it will be reset when K-9 Mail is restarted.
|
||||
*/
|
||||
@get:Synchronized
|
||||
var lastSelectedFolderId: Long? = null
|
||||
private set
|
||||
|
||||
@get:Synchronized
|
||||
@set:Synchronized
|
||||
var identities: MutableList<Identity> = mutableListOf()
|
||||
set(value) {
|
||||
field = value.toMutableList()
|
||||
}
|
||||
|
||||
@get:Synchronized
|
||||
var notificationSettings = NotificationSettings()
|
||||
private set
|
||||
|
||||
val displayName: String
|
||||
get() = name ?: email
|
||||
|
||||
@get:Synchronized
|
||||
@set:Synchronized
|
||||
override var email: String
|
||||
get() = identities[0].email!!
|
||||
set(email) {
|
||||
val newIdentity = identities[0].withEmail(email)
|
||||
identities[0] = newIdentity
|
||||
}
|
||||
|
||||
@get:Synchronized
|
||||
@set:Synchronized
|
||||
var senderName: String?
|
||||
get() = identities[0].name
|
||||
set(name) {
|
||||
val newIdentity = identities[0].withName(name)
|
||||
identities[0] = newIdentity
|
||||
}
|
||||
|
||||
@get:Synchronized
|
||||
@set:Synchronized
|
||||
var signatureUse: Boolean
|
||||
get() = identities[0].signatureUse
|
||||
set(signatureUse) {
|
||||
val newIdentity = identities[0].withSignatureUse(signatureUse)
|
||||
identities[0] = newIdentity
|
||||
}
|
||||
|
||||
@get:Synchronized
|
||||
@set:Synchronized
|
||||
var signature: String?
|
||||
get() = identities[0].signature
|
||||
set(signature) {
|
||||
val newIdentity = identities[0].withSignature(signature)
|
||||
identities[0] = newIdentity
|
||||
}
|
||||
|
||||
@get:JvmName("shouldMigrateToOAuth")
|
||||
@get:Synchronized
|
||||
@set:Synchronized
|
||||
var shouldMigrateToOAuth = false
|
||||
|
||||
/**
|
||||
* @param automaticCheckIntervalMinutes or -1 for never.
|
||||
*/
|
||||
@Synchronized
|
||||
fun updateAutomaticCheckIntervalMinutes(automaticCheckIntervalMinutes: Int): Boolean {
|
||||
val oldInterval = this.automaticCheckIntervalMinutes
|
||||
this.automaticCheckIntervalMinutes = automaticCheckIntervalMinutes
|
||||
|
||||
return oldInterval != automaticCheckIntervalMinutes
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun setDraftsFolderId(folderId: Long?, selection: SpecialFolderSelection) {
|
||||
draftsFolderId = folderId
|
||||
draftsFolderSelection = selection
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun hasDraftsFolder(): Boolean {
|
||||
return draftsFolderId != null
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun setSentFolderId(folderId: Long?, selection: SpecialFolderSelection) {
|
||||
sentFolderId = folderId
|
||||
sentFolderSelection = selection
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun hasSentFolder(): Boolean {
|
||||
return sentFolderId != null
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun setTrashFolderId(folderId: Long?, selection: SpecialFolderSelection) {
|
||||
trashFolderId = folderId
|
||||
trashFolderSelection = selection
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun hasTrashFolder(): Boolean {
|
||||
return trashFolderId != null
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun setArchiveFolderId(folderId: Long?, selection: SpecialFolderSelection) {
|
||||
archiveFolderId = folderId
|
||||
archiveFolderSelection = selection
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun hasArchiveFolder(): Boolean {
|
||||
return archiveFolderId != null
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun setSpamFolderId(folderId: Long?, selection: SpecialFolderSelection) {
|
||||
spamFolderId = folderId
|
||||
spamFolderSelection = selection
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun hasSpamFolder(): Boolean {
|
||||
return spamFolderId != null
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun updateFolderSyncMode(syncMode: FolderMode): Boolean {
|
||||
val oldSyncMode = folderSyncMode
|
||||
folderSyncMode = syncMode
|
||||
|
||||
return (oldSyncMode == FolderMode.NONE && syncMode != FolderMode.NONE) ||
|
||||
(oldSyncMode != FolderMode.NONE && syncMode == FolderMode.NONE)
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun isSortAscending(sortType: SortType): Boolean {
|
||||
return sortAscending.getOrPut(sortType) { sortType.isDefaultAscending }
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun setSortAscending(sortType: SortType, sortAscending: Boolean) {
|
||||
this.sortAscending[sortType] = sortAscending
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun replaceIdentities(identities: List<Identity>) {
|
||||
this.identities = identities.toMutableList()
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun getIdentity(index: Int): Identity {
|
||||
if (index !in identities.indices) error("Identity with index $index not found")
|
||||
|
||||
return identities[index]
|
||||
}
|
||||
|
||||
fun isAnIdentity(addresses: Array<Address>?): Boolean {
|
||||
if (addresses == null) return false
|
||||
|
||||
return addresses.any { address -> isAnIdentity(address) }
|
||||
}
|
||||
|
||||
fun isAnIdentity(address: Address): Boolean {
|
||||
return findIdentity(address) != null
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun findIdentity(address: Address): Identity? {
|
||||
return identities.find { identity ->
|
||||
identity.email.equals(address.address, ignoreCase = true)
|
||||
}
|
||||
}
|
||||
|
||||
val earliestPollDate: Date?
|
||||
get() {
|
||||
val age = maximumPolledMessageAge.takeIf { it >= 0 } ?: return null
|
||||
|
||||
val now = Calendar.getInstance()
|
||||
now[Calendar.HOUR_OF_DAY] = 0
|
||||
now[Calendar.MINUTE] = 0
|
||||
now[Calendar.SECOND] = 0
|
||||
now[Calendar.MILLISECOND] = 0
|
||||
|
||||
if (age < 28) {
|
||||
now.add(Calendar.DATE, age * -1)
|
||||
} else {
|
||||
when (age) {
|
||||
28 -> now.add(Calendar.MONTH, -1)
|
||||
56 -> now.add(Calendar.MONTH, -2)
|
||||
84 -> now.add(Calendar.MONTH, -3)
|
||||
168 -> now.add(Calendar.MONTH, -6)
|
||||
365 -> now.add(Calendar.YEAR, -1)
|
||||
}
|
||||
}
|
||||
|
||||
return now.time
|
||||
}
|
||||
|
||||
val isOpenPgpProviderConfigured: Boolean
|
||||
get() = openPgpProvider != null
|
||||
|
||||
@Synchronized
|
||||
fun hasOpenPgpKey(): Boolean {
|
||||
return openPgpKey != NO_OPENPGP_KEY
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun setLastSelectedFolderId(folderId: Long) {
|
||||
lastSelectedFolderId = folderId
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun resetChangeMarkers() {
|
||||
isChangedVisibleLimits = false
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun markSetupFinished() {
|
||||
isFinishedSetup = true
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun updateNotificationSettings(block: (oldNotificationSettings: NotificationSettings) -> NotificationSettings) {
|
||||
notificationSettings = block(notificationSettings)
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return if (K9.isSensitiveDebugLoggingEnabled) displayName else uuid
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
return if (other is Account) {
|
||||
other.uuid == uuid
|
||||
} else {
|
||||
super.equals(other)
|
||||
}
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
return uuid.hashCode()
|
||||
}
|
||||
|
||||
enum class FolderMode {
|
||||
NONE,
|
||||
ALL,
|
||||
FIRST_CLASS,
|
||||
FIRST_AND_SECOND_CLASS,
|
||||
NOT_SECOND_CLASS
|
||||
}
|
||||
|
||||
enum class SpecialFolderSelection {
|
||||
AUTOMATIC,
|
||||
MANUAL
|
||||
}
|
||||
|
||||
enum class ShowPictures {
|
||||
NEVER,
|
||||
ALWAYS,
|
||||
ONLY_FROM_CONTACTS
|
||||
}
|
||||
|
||||
enum class Searchable {
|
||||
ALL,
|
||||
DISPLAYABLE,
|
||||
NONE
|
||||
}
|
||||
|
||||
enum class QuoteStyle {
|
||||
PREFIX,
|
||||
HEADER
|
||||
}
|
||||
|
||||
enum class MessageFormat {
|
||||
TEXT,
|
||||
HTML,
|
||||
AUTO
|
||||
}
|
||||
|
||||
enum class Expunge {
|
||||
EXPUNGE_IMMEDIATELY,
|
||||
EXPUNGE_MANUALLY,
|
||||
EXPUNGE_ON_POLL;
|
||||
|
||||
fun toBackendExpungePolicy(): ExpungePolicy = when (this) {
|
||||
EXPUNGE_IMMEDIATELY -> ExpungePolicy.IMMEDIATELY
|
||||
EXPUNGE_MANUALLY -> ExpungePolicy.MANUALLY
|
||||
EXPUNGE_ON_POLL -> ExpungePolicy.ON_POLL
|
||||
}
|
||||
}
|
||||
|
||||
enum class DeletePolicy(@JvmField val setting: Int) {
|
||||
NEVER(0),
|
||||
SEVEN_DAYS(1),
|
||||
ON_DELETE(2),
|
||||
MARK_AS_READ(3);
|
||||
|
||||
companion object {
|
||||
fun fromInt(initialSetting: Int): DeletePolicy {
|
||||
return values().find { it.setting == initialSetting } ?: error("DeletePolicy $initialSetting unknown")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum class SortType(val isDefaultAscending: Boolean) {
|
||||
SORT_DATE(false),
|
||||
SORT_ARRIVAL(false),
|
||||
SORT_SUBJECT(true),
|
||||
SORT_SENDER(true),
|
||||
SORT_UNREAD(true),
|
||||
SORT_FLAGGED(true),
|
||||
SORT_ATTACHMENT(true);
|
||||
}
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* Fixed name of outbox - not actually displayed.
|
||||
*/
|
||||
const val OUTBOX_NAME = "Outbox"
|
||||
|
||||
@JvmField
|
||||
val DEFAULT_SORT_TYPE = SortType.SORT_DATE
|
||||
const val DEFAULT_SORT_ASCENDING = false
|
||||
const val NO_OPENPGP_KEY: Long = 0
|
||||
const val UNASSIGNED_ACCOUNT_NUMBER = -1
|
||||
const val INTERVAL_MINUTES_NEVER = -1
|
||||
const val DEFAULT_SYNC_INTERVAL = 60
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,648 @@
|
|||
package com.fsck.k9
|
||||
|
||||
import com.fsck.k9.Account.Companion.DEFAULT_SORT_ASCENDING
|
||||
import com.fsck.k9.Account.Companion.DEFAULT_SORT_TYPE
|
||||
import com.fsck.k9.Account.Companion.DEFAULT_SYNC_INTERVAL
|
||||
import com.fsck.k9.Account.Companion.NO_OPENPGP_KEY
|
||||
import com.fsck.k9.Account.Companion.UNASSIGNED_ACCOUNT_NUMBER
|
||||
import com.fsck.k9.Account.DeletePolicy
|
||||
import com.fsck.k9.Account.Expunge
|
||||
import com.fsck.k9.Account.FolderMode
|
||||
import com.fsck.k9.Account.MessageFormat
|
||||
import com.fsck.k9.Account.QuoteStyle
|
||||
import com.fsck.k9.Account.Searchable
|
||||
import com.fsck.k9.Account.ShowPictures
|
||||
import com.fsck.k9.Account.SortType
|
||||
import com.fsck.k9.Account.SpecialFolderSelection
|
||||
import com.fsck.k9.helper.Utility
|
||||
import com.fsck.k9.mailstore.StorageManager
|
||||
import com.fsck.k9.preferences.Storage
|
||||
import com.fsck.k9.preferences.StorageEditor
|
||||
import timber.log.Timber
|
||||
|
||||
class AccountPreferenceSerializer(
|
||||
private val storageManager: StorageManager,
|
||||
private val resourceProvider: CoreResourceProvider,
|
||||
private val serverSettingsSerializer: ServerSettingsSerializer
|
||||
) {
|
||||
|
||||
@Synchronized
|
||||
fun loadAccount(account: Account, storage: Storage) {
|
||||
val accountUuid = account.uuid
|
||||
with(account) {
|
||||
incomingServerSettings = serverSettingsSerializer.deserialize(
|
||||
storage.getString("$accountUuid.$INCOMING_SERVER_SETTINGS_KEY", "")
|
||||
)
|
||||
outgoingServerSettings = serverSettingsSerializer.deserialize(
|
||||
storage.getString("$accountUuid.$OUTGOING_SERVER_SETTINGS_KEY", "")
|
||||
)
|
||||
oAuthState = storage.getString("$accountUuid.oAuthState", null)
|
||||
localStorageProviderId = storage.getString("$accountUuid.localStorageProvider", storageManager.defaultProviderId)
|
||||
name = storage.getString("$accountUuid.description", null)
|
||||
alwaysBcc = storage.getString("$accountUuid.alwaysBcc", alwaysBcc)
|
||||
automaticCheckIntervalMinutes = storage.getInt("$accountUuid.automaticCheckIntervalMinutes", DEFAULT_SYNC_INTERVAL)
|
||||
idleRefreshMinutes = storage.getInt("$accountUuid.idleRefreshMinutes", 24)
|
||||
displayCount = storage.getInt("$accountUuid.displayCount", K9.DEFAULT_VISIBLE_LIMIT)
|
||||
if (displayCount < 0) {
|
||||
displayCount = K9.DEFAULT_VISIBLE_LIMIT
|
||||
}
|
||||
isNotifyNewMail = storage.getBoolean("$accountUuid.notifyNewMail", false)
|
||||
|
||||
folderNotifyNewMailMode = getEnumStringPref<FolderMode>(storage, "$accountUuid.folderNotifyNewMailMode", FolderMode.ALL)
|
||||
isNotifySelfNewMail = storage.getBoolean("$accountUuid.notifySelfNewMail", true)
|
||||
isNotifyContactsMailOnly = storage.getBoolean("$accountUuid.notifyContactsMailOnly", false)
|
||||
isIgnoreChatMessages = storage.getBoolean("$accountUuid.ignoreChatMessages", false)
|
||||
isNotifySync = storage.getBoolean("$accountUuid.notifyMailCheck", false)
|
||||
messagesNotificationChannelVersion = storage.getInt("$accountUuid.messagesNotificationChannelVersion", 0)
|
||||
deletePolicy = DeletePolicy.fromInt(storage.getInt("$accountUuid.deletePolicy", DeletePolicy.NEVER.setting))
|
||||
legacyInboxFolder = storage.getString("$accountUuid.inboxFolderName", null)
|
||||
importedDraftsFolder = storage.getString("$accountUuid.draftsFolderName", null)
|
||||
importedSentFolder = storage.getString("$accountUuid.sentFolderName", null)
|
||||
importedTrashFolder = storage.getString("$accountUuid.trashFolderName", null)
|
||||
importedArchiveFolder = storage.getString("$accountUuid.archiveFolderName", null)
|
||||
importedSpamFolder = storage.getString("$accountUuid.spamFolderName", null)
|
||||
|
||||
inboxFolderId = storage.getString("$accountUuid.inboxFolderId", null)?.toLongOrNull()
|
||||
outboxFolderId = storage.getString("$accountUuid.outboxFolderId", null)?.toLongOrNull()
|
||||
|
||||
val draftsFolderId = storage.getString("$accountUuid.draftsFolderId", null)?.toLongOrNull()
|
||||
val draftsFolderSelection = getEnumStringPref<SpecialFolderSelection>(
|
||||
storage,
|
||||
"$accountUuid.draftsFolderSelection",
|
||||
SpecialFolderSelection.AUTOMATIC
|
||||
)
|
||||
setDraftsFolderId(draftsFolderId, draftsFolderSelection)
|
||||
|
||||
val sentFolderId = storage.getString("$accountUuid.sentFolderId", null)?.toLongOrNull()
|
||||
val sentFolderSelection = getEnumStringPref<SpecialFolderSelection>(
|
||||
storage,
|
||||
"$accountUuid.sentFolderSelection",
|
||||
SpecialFolderSelection.AUTOMATIC
|
||||
)
|
||||
setSentFolderId(sentFolderId, sentFolderSelection)
|
||||
|
||||
val trashFolderId = storage.getString("$accountUuid.trashFolderId", null)?.toLongOrNull()
|
||||
val trashFolderSelection = getEnumStringPref<SpecialFolderSelection>(
|
||||
storage,
|
||||
"$accountUuid.trashFolderSelection",
|
||||
SpecialFolderSelection.AUTOMATIC
|
||||
)
|
||||
setTrashFolderId(trashFolderId, trashFolderSelection)
|
||||
|
||||
val archiveFolderId = storage.getString("$accountUuid.archiveFolderId", null)?.toLongOrNull()
|
||||
val archiveFolderSelection = getEnumStringPref<SpecialFolderSelection>(
|
||||
storage,
|
||||
"$accountUuid.archiveFolderSelection",
|
||||
SpecialFolderSelection.AUTOMATIC
|
||||
)
|
||||
setArchiveFolderId(archiveFolderId, archiveFolderSelection)
|
||||
|
||||
val spamFolderId = storage.getString("$accountUuid.spamFolderId", null)?.toLongOrNull()
|
||||
val spamFolderSelection = getEnumStringPref<SpecialFolderSelection>(
|
||||
storage,
|
||||
"$accountUuid.spamFolderSelection",
|
||||
SpecialFolderSelection.AUTOMATIC
|
||||
)
|
||||
setSpamFolderId(spamFolderId, spamFolderSelection)
|
||||
|
||||
autoExpandFolderId = storage.getString("$accountUuid.autoExpandFolderId", null)?.toLongOrNull()
|
||||
|
||||
expungePolicy = getEnumStringPref<Expunge>(storage, "$accountUuid.expungePolicy", Expunge.EXPUNGE_IMMEDIATELY)
|
||||
isSyncRemoteDeletions = storage.getBoolean("$accountUuid.syncRemoteDeletions", true)
|
||||
|
||||
maxPushFolders = storage.getInt("$accountUuid.maxPushFolders", 10)
|
||||
isSubscribedFoldersOnly = storage.getBoolean("$accountUuid.subscribedFoldersOnly", false)
|
||||
maximumPolledMessageAge = storage.getInt("$accountUuid.maximumPolledMessageAge", -1)
|
||||
maximumAutoDownloadMessageSize = storage.getInt("$accountUuid.maximumAutoDownloadMessageSize", 32768)
|
||||
messageFormat = getEnumStringPref<MessageFormat>(storage, "$accountUuid.messageFormat", DEFAULT_MESSAGE_FORMAT)
|
||||
val messageFormatAuto = storage.getBoolean("$accountUuid.messageFormatAuto", DEFAULT_MESSAGE_FORMAT_AUTO)
|
||||
if (messageFormatAuto && messageFormat == MessageFormat.TEXT) {
|
||||
messageFormat = MessageFormat.AUTO
|
||||
}
|
||||
isMessageReadReceipt = storage.getBoolean("$accountUuid.messageReadReceipt", DEFAULT_MESSAGE_READ_RECEIPT)
|
||||
quoteStyle = getEnumStringPref<QuoteStyle>(storage, "$accountUuid.quoteStyle", DEFAULT_QUOTE_STYLE)
|
||||
quotePrefix = storage.getString("$accountUuid.quotePrefix", DEFAULT_QUOTE_PREFIX)
|
||||
isDefaultQuotedTextShown = storage.getBoolean("$accountUuid.defaultQuotedTextShown", DEFAULT_QUOTED_TEXT_SHOWN)
|
||||
isReplyAfterQuote = storage.getBoolean("$accountUuid.replyAfterQuote", DEFAULT_REPLY_AFTER_QUOTE)
|
||||
isStripSignature = storage.getBoolean("$accountUuid.stripSignature", DEFAULT_STRIP_SIGNATURE)
|
||||
useCompression = storage.getBoolean("$accountUuid.useCompression", true)
|
||||
|
||||
importedAutoExpandFolder = storage.getString("$accountUuid.autoExpandFolderName", null)
|
||||
|
||||
accountNumber = storage.getInt("$accountUuid.accountNumber", UNASSIGNED_ACCOUNT_NUMBER)
|
||||
|
||||
chipColor = storage.getInt("$accountUuid.chipColor", FALLBACK_ACCOUNT_COLOR)
|
||||
|
||||
sortType = getEnumStringPref<SortType>(storage, "$accountUuid.sortTypeEnum", SortType.SORT_DATE)
|
||||
|
||||
setSortAscending(sortType, storage.getBoolean("$accountUuid.sortAscending", false))
|
||||
|
||||
showPictures = getEnumStringPref<ShowPictures>(storage, "$accountUuid.showPicturesEnum", ShowPictures.NEVER)
|
||||
|
||||
updateNotificationSettings {
|
||||
NotificationSettings(
|
||||
isRingEnabled = storage.getBoolean("$accountUuid.ring", true),
|
||||
ringtone = storage.getString("$accountUuid.ringtone", DEFAULT_RINGTONE_URI),
|
||||
light = getEnumStringPref(storage, "$accountUuid.notificationLight", NotificationLight.Disabled),
|
||||
vibration = NotificationVibration(
|
||||
isEnabled = storage.getBoolean("$accountUuid.vibrate", false),
|
||||
pattern = VibratePattern.deserialize(storage.getInt("$accountUuid.vibratePattern", 0)),
|
||||
repeatCount = storage.getInt("$accountUuid.vibrateTimes", 5)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
folderDisplayMode = getEnumStringPref<FolderMode>(storage, "$accountUuid.folderDisplayMode", FolderMode.NOT_SECOND_CLASS)
|
||||
|
||||
folderSyncMode = getEnumStringPref<FolderMode>(storage, "$accountUuid.folderSyncMode", FolderMode.FIRST_CLASS)
|
||||
|
||||
folderPushMode = getEnumStringPref<FolderMode>(storage, "$accountUuid.folderPushMode", FolderMode.NONE)
|
||||
|
||||
folderTargetMode = getEnumStringPref<FolderMode>(storage, "$accountUuid.folderTargetMode", FolderMode.NOT_SECOND_CLASS)
|
||||
|
||||
searchableFolders = getEnumStringPref<Searchable>(storage, "$accountUuid.searchableFolders", Searchable.ALL)
|
||||
|
||||
isSignatureBeforeQuotedText = storage.getBoolean("$accountUuid.signatureBeforeQuotedText", false)
|
||||
replaceIdentities(loadIdentities(accountUuid, storage))
|
||||
|
||||
openPgpProvider = storage.getString("$accountUuid.openPgpProvider", "")
|
||||
openPgpKey = storage.getLong("$accountUuid.cryptoKey", NO_OPENPGP_KEY)
|
||||
isOpenPgpHideSignOnly = storage.getBoolean("$accountUuid.openPgpHideSignOnly", true)
|
||||
isOpenPgpEncryptSubject = storage.getBoolean("$accountUuid.openPgpEncryptSubject", true)
|
||||
isOpenPgpEncryptAllDrafts = storage.getBoolean("$accountUuid.openPgpEncryptAllDrafts", true)
|
||||
autocryptPreferEncryptMutual = storage.getBoolean("$accountUuid.autocryptMutualMode", false)
|
||||
isRemoteSearchFullText = storage.getBoolean("$accountUuid.remoteSearchFullText", false)
|
||||
remoteSearchNumResults = storage.getInt("$accountUuid.remoteSearchNumResults", DEFAULT_REMOTE_SEARCH_NUM_RESULTS)
|
||||
isUploadSentMessages = storage.getBoolean("$accountUuid.uploadSentMessages", true)
|
||||
|
||||
isMarkMessageAsReadOnView = storage.getBoolean("$accountUuid.markMessageAsReadOnView", true)
|
||||
isMarkMessageAsReadOnDelete = storage.getBoolean("$accountUuid.markMessageAsReadOnDelete", true)
|
||||
isAlwaysShowCcBcc = storage.getBoolean("$accountUuid.alwaysShowCcBcc", false)
|
||||
lastSyncTime = storage.getLong("$accountUuid.lastSyncTime", 0L)
|
||||
lastFolderListRefreshTime = storage.getLong("$accountUuid.lastFolderListRefreshTime", 0L)
|
||||
|
||||
shouldMigrateToOAuth = storage.getBoolean("$accountUuid.migrateToOAuth", false)
|
||||
|
||||
val isFinishedSetup = storage.getBoolean("$accountUuid.isFinishedSetup", true)
|
||||
if (isFinishedSetup) markSetupFinished()
|
||||
|
||||
resetChangeMarkers()
|
||||
}
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
private fun loadIdentities(accountUuid: String, storage: Storage): List<Identity> {
|
||||
val newIdentities = ArrayList<Identity>()
|
||||
var ident = 0
|
||||
var gotOne: Boolean
|
||||
do {
|
||||
gotOne = false
|
||||
val name = storage.getString("$accountUuid.$IDENTITY_NAME_KEY.$ident", null)
|
||||
val email = storage.getString("$accountUuid.$IDENTITY_EMAIL_KEY.$ident", null)
|
||||
val signatureUse = storage.getBoolean("$accountUuid.signatureUse.$ident", false)
|
||||
val signature = storage.getString("$accountUuid.signature.$ident", null)
|
||||
val description = storage.getString("$accountUuid.$IDENTITY_DESCRIPTION_KEY.$ident", null)
|
||||
val replyTo = storage.getString("$accountUuid.replyTo.$ident", null)
|
||||
if (email != null) {
|
||||
val identity = Identity(
|
||||
name = name,
|
||||
email = email,
|
||||
signatureUse = signatureUse,
|
||||
signature = signature,
|
||||
description = description,
|
||||
replyTo = replyTo
|
||||
)
|
||||
newIdentities.add(identity)
|
||||
gotOne = true
|
||||
}
|
||||
ident++
|
||||
} while (gotOne)
|
||||
|
||||
if (newIdentities.isEmpty()) {
|
||||
val name = storage.getString("$accountUuid.name", null)
|
||||
val email = storage.getString("$accountUuid.email", null)
|
||||
val signatureUse = storage.getBoolean("$accountUuid.signatureUse", false)
|
||||
val signature = storage.getString("$accountUuid.signature", null)
|
||||
val identity = Identity(
|
||||
name = name,
|
||||
email = email,
|
||||
signatureUse = signatureUse,
|
||||
signature = signature,
|
||||
description = email
|
||||
)
|
||||
newIdentities.add(identity)
|
||||
}
|
||||
|
||||
return newIdentities
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun save(editor: StorageEditor, storage: Storage, account: Account) {
|
||||
val accountUuid = account.uuid
|
||||
|
||||
if (!storage.getString("accountUuids", "").contains(account.uuid)) {
|
||||
var accountUuids = storage.getString("accountUuids", "")
|
||||
accountUuids += (if (accountUuids.isNotEmpty()) "," else "") + account.uuid
|
||||
editor.putString("accountUuids", accountUuids)
|
||||
}
|
||||
|
||||
with(account) {
|
||||
editor.putString("$accountUuid.$INCOMING_SERVER_SETTINGS_KEY", serverSettingsSerializer.serialize(incomingServerSettings))
|
||||
editor.putString("$accountUuid.$OUTGOING_SERVER_SETTINGS_KEY", serverSettingsSerializer.serialize(outgoingServerSettings))
|
||||
editor.putString("$accountUuid.oAuthState", oAuthState)
|
||||
editor.putString("$accountUuid.localStorageProvider", localStorageProviderId)
|
||||
editor.putString("$accountUuid.description", name)
|
||||
editor.putString("$accountUuid.alwaysBcc", alwaysBcc)
|
||||
editor.putInt("$accountUuid.automaticCheckIntervalMinutes", automaticCheckIntervalMinutes)
|
||||
editor.putInt("$accountUuid.idleRefreshMinutes", idleRefreshMinutes)
|
||||
editor.putInt("$accountUuid.displayCount", displayCount)
|
||||
editor.putBoolean("$accountUuid.notifyNewMail", isNotifyNewMail)
|
||||
editor.putString("$accountUuid.folderNotifyNewMailMode", folderNotifyNewMailMode.name)
|
||||
editor.putBoolean("$accountUuid.notifySelfNewMail", isNotifySelfNewMail)
|
||||
editor.putBoolean("$accountUuid.notifyContactsMailOnly", isNotifyContactsMailOnly)
|
||||
editor.putBoolean("$accountUuid.ignoreChatMessages", isIgnoreChatMessages)
|
||||
editor.putBoolean("$accountUuid.notifyMailCheck", isNotifySync)
|
||||
editor.putInt("$accountUuid.messagesNotificationChannelVersion", messagesNotificationChannelVersion)
|
||||
editor.putInt("$accountUuid.deletePolicy", deletePolicy.setting)
|
||||
editor.putString("$accountUuid.inboxFolderName", legacyInboxFolder)
|
||||
editor.putString("$accountUuid.draftsFolderName", importedDraftsFolder)
|
||||
editor.putString("$accountUuid.sentFolderName", importedSentFolder)
|
||||
editor.putString("$accountUuid.trashFolderName", importedTrashFolder)
|
||||
editor.putString("$accountUuid.archiveFolderName", importedArchiveFolder)
|
||||
editor.putString("$accountUuid.spamFolderName", importedSpamFolder)
|
||||
editor.putString("$accountUuid.inboxFolderId", inboxFolderId?.toString())
|
||||
editor.putString("$accountUuid.outboxFolderId", outboxFolderId?.toString())
|
||||
editor.putString("$accountUuid.draftsFolderId", draftsFolderId?.toString())
|
||||
editor.putString("$accountUuid.sentFolderId", sentFolderId?.toString())
|
||||
editor.putString("$accountUuid.trashFolderId", trashFolderId?.toString())
|
||||
editor.putString("$accountUuid.archiveFolderId", archiveFolderId?.toString())
|
||||
editor.putString("$accountUuid.spamFolderId", spamFolderId?.toString())
|
||||
editor.putString("$accountUuid.archiveFolderSelection", archiveFolderSelection.name)
|
||||
editor.putString("$accountUuid.draftsFolderSelection", draftsFolderSelection.name)
|
||||
editor.putString("$accountUuid.sentFolderSelection", sentFolderSelection.name)
|
||||
editor.putString("$accountUuid.spamFolderSelection", spamFolderSelection.name)
|
||||
editor.putString("$accountUuid.trashFolderSelection", trashFolderSelection.name)
|
||||
editor.putString("$accountUuid.autoExpandFolderName", importedAutoExpandFolder)
|
||||
editor.putString("$accountUuid.autoExpandFolderId", autoExpandFolderId?.toString())
|
||||
editor.putInt("$accountUuid.accountNumber", accountNumber)
|
||||
editor.putString("$accountUuid.sortTypeEnum", sortType.name)
|
||||
editor.putBoolean("$accountUuid.sortAscending", isSortAscending(sortType))
|
||||
editor.putString("$accountUuid.showPicturesEnum", showPictures.name)
|
||||
editor.putString("$accountUuid.folderDisplayMode", folderDisplayMode.name)
|
||||
editor.putString("$accountUuid.folderSyncMode", folderSyncMode.name)
|
||||
editor.putString("$accountUuid.folderPushMode", folderPushMode.name)
|
||||
editor.putString("$accountUuid.folderTargetMode", folderTargetMode.name)
|
||||
editor.putBoolean("$accountUuid.signatureBeforeQuotedText", isSignatureBeforeQuotedText)
|
||||
editor.putString("$accountUuid.expungePolicy", expungePolicy.name)
|
||||
editor.putBoolean("$accountUuid.syncRemoteDeletions", isSyncRemoteDeletions)
|
||||
editor.putInt("$accountUuid.maxPushFolders", maxPushFolders)
|
||||
editor.putString("$accountUuid.searchableFolders", searchableFolders.name)
|
||||
editor.putInt("$accountUuid.chipColor", chipColor)
|
||||
editor.putBoolean("$accountUuid.subscribedFoldersOnly", isSubscribedFoldersOnly)
|
||||
editor.putInt("$accountUuid.maximumPolledMessageAge", maximumPolledMessageAge)
|
||||
editor.putInt("$accountUuid.maximumAutoDownloadMessageSize", maximumAutoDownloadMessageSize)
|
||||
val messageFormatAuto = if (MessageFormat.AUTO == messageFormat) {
|
||||
// saving MessageFormat.AUTO as is to the database will cause downgrades to crash on
|
||||
// startup, so we save as MessageFormat.TEXT instead with a separate flag for auto.
|
||||
editor.putString("$accountUuid.messageFormat", MessageFormat.TEXT.name)
|
||||
true
|
||||
} else {
|
||||
editor.putString("$accountUuid.messageFormat", messageFormat.name)
|
||||
false
|
||||
}
|
||||
editor.putBoolean("$accountUuid.messageFormatAuto", messageFormatAuto)
|
||||
editor.putBoolean("$accountUuid.messageReadReceipt", isMessageReadReceipt)
|
||||
editor.putString("$accountUuid.quoteStyle", quoteStyle.name)
|
||||
editor.putString("$accountUuid.quotePrefix", quotePrefix)
|
||||
editor.putBoolean("$accountUuid.defaultQuotedTextShown", isDefaultQuotedTextShown)
|
||||
editor.putBoolean("$accountUuid.replyAfterQuote", isReplyAfterQuote)
|
||||
editor.putBoolean("$accountUuid.stripSignature", isStripSignature)
|
||||
editor.putLong("$accountUuid.cryptoKey", openPgpKey)
|
||||
editor.putBoolean("$accountUuid.openPgpHideSignOnly", isOpenPgpHideSignOnly)
|
||||
editor.putBoolean("$accountUuid.openPgpEncryptSubject", isOpenPgpEncryptSubject)
|
||||
editor.putBoolean("$accountUuid.openPgpEncryptAllDrafts", isOpenPgpEncryptAllDrafts)
|
||||
editor.putString("$accountUuid.openPgpProvider", openPgpProvider)
|
||||
editor.putBoolean("$accountUuid.autocryptMutualMode", autocryptPreferEncryptMutual)
|
||||
editor.putBoolean("$accountUuid.remoteSearchFullText", isRemoteSearchFullText)
|
||||
editor.putInt("$accountUuid.remoteSearchNumResults", remoteSearchNumResults)
|
||||
editor.putBoolean("$accountUuid.uploadSentMessages", isUploadSentMessages)
|
||||
editor.putBoolean("$accountUuid.markMessageAsReadOnView", isMarkMessageAsReadOnView)
|
||||
editor.putBoolean("$accountUuid.markMessageAsReadOnDelete", isMarkMessageAsReadOnDelete)
|
||||
editor.putBoolean("$accountUuid.alwaysShowCcBcc", isAlwaysShowCcBcc)
|
||||
|
||||
editor.putBoolean("$accountUuid.vibrate", notificationSettings.vibration.isEnabled)
|
||||
editor.putInt("$accountUuid.vibratePattern", notificationSettings.vibration.pattern.serialize())
|
||||
editor.putInt("$accountUuid.vibrateTimes", notificationSettings.vibration.repeatCount)
|
||||
editor.putBoolean("$accountUuid.ring", notificationSettings.isRingEnabled)
|
||||
editor.putString("$accountUuid.ringtone", notificationSettings.ringtone)
|
||||
editor.putString("$accountUuid.notificationLight", notificationSettings.light.name)
|
||||
editor.putLong("$accountUuid.lastSyncTime", lastSyncTime)
|
||||
editor.putLong("$accountUuid.lastFolderListRefreshTime", lastFolderListRefreshTime)
|
||||
editor.putBoolean("$accountUuid.isFinishedSetup", isFinishedSetup)
|
||||
editor.putBoolean("$accountUuid.useCompression", useCompression)
|
||||
editor.putBoolean("$accountUuid.migrateToOAuth", shouldMigrateToOAuth)
|
||||
}
|
||||
|
||||
saveIdentities(account, storage, editor)
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun delete(editor: StorageEditor, storage: Storage, account: Account) {
|
||||
val accountUuid = account.uuid
|
||||
|
||||
// Get the list of account UUIDs
|
||||
val uuids = storage.getString("accountUuids", "").split(",".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
|
||||
|
||||
// Create a list of all account UUIDs excluding this account
|
||||
val newUuids = ArrayList<String>(uuids.size)
|
||||
for (uuid in uuids) {
|
||||
if (uuid != accountUuid) {
|
||||
newUuids.add(uuid)
|
||||
}
|
||||
}
|
||||
|
||||
// Only change the 'accountUuids' value if this account's UUID was listed before
|
||||
if (newUuids.size < uuids.size) {
|
||||
val accountUuids = Utility.combine(newUuids.toTypedArray(), ',')
|
||||
editor.putString("accountUuids", accountUuids)
|
||||
}
|
||||
|
||||
editor.remove("$accountUuid.$INCOMING_SERVER_SETTINGS_KEY")
|
||||
editor.remove("$accountUuid.$OUTGOING_SERVER_SETTINGS_KEY")
|
||||
editor.remove("$accountUuid.oAuthState")
|
||||
editor.remove("$accountUuid.description")
|
||||
editor.remove("$accountUuid.name")
|
||||
editor.remove("$accountUuid.email")
|
||||
editor.remove("$accountUuid.alwaysBcc")
|
||||
editor.remove("$accountUuid.automaticCheckIntervalMinutes")
|
||||
editor.remove("$accountUuid.idleRefreshMinutes")
|
||||
editor.remove("$accountUuid.lastAutomaticCheckTime")
|
||||
editor.remove("$accountUuid.notifyNewMail")
|
||||
editor.remove("$accountUuid.notifySelfNewMail")
|
||||
editor.remove("$accountUuid.ignoreChatMessages")
|
||||
editor.remove("$accountUuid.messagesNotificationChannelVersion")
|
||||
editor.remove("$accountUuid.deletePolicy")
|
||||
editor.remove("$accountUuid.draftsFolderName")
|
||||
editor.remove("$accountUuid.sentFolderName")
|
||||
editor.remove("$accountUuid.trashFolderName")
|
||||
editor.remove("$accountUuid.archiveFolderName")
|
||||
editor.remove("$accountUuid.spamFolderName")
|
||||
editor.remove("$accountUuid.archiveFolderSelection")
|
||||
editor.remove("$accountUuid.draftsFolderSelection")
|
||||
editor.remove("$accountUuid.sentFolderSelection")
|
||||
editor.remove("$accountUuid.spamFolderSelection")
|
||||
editor.remove("$accountUuid.trashFolderSelection")
|
||||
editor.remove("$accountUuid.autoExpandFolderName")
|
||||
editor.remove("$accountUuid.accountNumber")
|
||||
editor.remove("$accountUuid.vibrate")
|
||||
editor.remove("$accountUuid.vibratePattern")
|
||||
editor.remove("$accountUuid.vibrateTimes")
|
||||
editor.remove("$accountUuid.ring")
|
||||
editor.remove("$accountUuid.ringtone")
|
||||
editor.remove("$accountUuid.folderDisplayMode")
|
||||
editor.remove("$accountUuid.folderSyncMode")
|
||||
editor.remove("$accountUuid.folderPushMode")
|
||||
editor.remove("$accountUuid.folderTargetMode")
|
||||
editor.remove("$accountUuid.signatureBeforeQuotedText")
|
||||
editor.remove("$accountUuid.expungePolicy")
|
||||
editor.remove("$accountUuid.syncRemoteDeletions")
|
||||
editor.remove("$accountUuid.maxPushFolders")
|
||||
editor.remove("$accountUuid.searchableFolders")
|
||||
editor.remove("$accountUuid.chipColor")
|
||||
editor.remove("$accountUuid.notificationLight")
|
||||
editor.remove("$accountUuid.subscribedFoldersOnly")
|
||||
editor.remove("$accountUuid.maximumPolledMessageAge")
|
||||
editor.remove("$accountUuid.maximumAutoDownloadMessageSize")
|
||||
editor.remove("$accountUuid.messageFormatAuto")
|
||||
editor.remove("$accountUuid.quoteStyle")
|
||||
editor.remove("$accountUuid.quotePrefix")
|
||||
editor.remove("$accountUuid.sortTypeEnum")
|
||||
editor.remove("$accountUuid.sortAscending")
|
||||
editor.remove("$accountUuid.showPicturesEnum")
|
||||
editor.remove("$accountUuid.replyAfterQuote")
|
||||
editor.remove("$accountUuid.stripSignature")
|
||||
editor.remove("$accountUuid.cryptoApp") // this is no longer set, but cleans up legacy values
|
||||
editor.remove("$accountUuid.cryptoAutoSignature")
|
||||
editor.remove("$accountUuid.cryptoAutoEncrypt")
|
||||
editor.remove("$accountUuid.cryptoApp")
|
||||
editor.remove("$accountUuid.cryptoKey")
|
||||
editor.remove("$accountUuid.cryptoSupportSignOnly")
|
||||
editor.remove("$accountUuid.openPgpProvider")
|
||||
editor.remove("$accountUuid.openPgpHideSignOnly")
|
||||
editor.remove("$accountUuid.openPgpEncryptSubject")
|
||||
editor.remove("$accountUuid.openPgpEncryptAllDrafts")
|
||||
editor.remove("$accountUuid.autocryptMutualMode")
|
||||
editor.remove("$accountUuid.enabled")
|
||||
editor.remove("$accountUuid.markMessageAsReadOnView")
|
||||
editor.remove("$accountUuid.markMessageAsReadOnDelete")
|
||||
editor.remove("$accountUuid.alwaysShowCcBcc")
|
||||
editor.remove("$accountUuid.remoteSearchFullText")
|
||||
editor.remove("$accountUuid.remoteSearchNumResults")
|
||||
editor.remove("$accountUuid.uploadSentMessages")
|
||||
editor.remove("$accountUuid.defaultQuotedTextShown")
|
||||
editor.remove("$accountUuid.displayCount")
|
||||
editor.remove("$accountUuid.inboxFolderName")
|
||||
editor.remove("$accountUuid.localStorageProvider")
|
||||
editor.remove("$accountUuid.messageFormat")
|
||||
editor.remove("$accountUuid.messageReadReceipt")
|
||||
editor.remove("$accountUuid.notifyMailCheck")
|
||||
editor.remove("$accountUuid.inboxFolderId")
|
||||
editor.remove("$accountUuid.outboxFolderId")
|
||||
editor.remove("$accountUuid.draftsFolderId")
|
||||
editor.remove("$accountUuid.sentFolderId")
|
||||
editor.remove("$accountUuid.trashFolderId")
|
||||
editor.remove("$accountUuid.archiveFolderId")
|
||||
editor.remove("$accountUuid.spamFolderId")
|
||||
editor.remove("$accountUuid.autoExpandFolderId")
|
||||
editor.remove("$accountUuid.lastSyncTime")
|
||||
editor.remove("$accountUuid.lastFolderListRefreshTime")
|
||||
editor.remove("$accountUuid.isFinishedSetup")
|
||||
editor.remove("$accountUuid.useCompression")
|
||||
editor.remove("$accountUuid.migrateToOAuth")
|
||||
|
||||
deleteIdentities(account, storage, editor)
|
||||
// TODO: Remove preference settings that may exist for individual folders in the account.
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
private fun saveIdentities(account: Account, storage: Storage, editor: StorageEditor) {
|
||||
deleteIdentities(account, storage, editor)
|
||||
var ident = 0
|
||||
|
||||
with(account) {
|
||||
for (identity in identities) {
|
||||
editor.putString("$uuid.$IDENTITY_NAME_KEY.$ident", identity.name)
|
||||
editor.putString("$uuid.$IDENTITY_EMAIL_KEY.$ident", identity.email)
|
||||
editor.putBoolean("$uuid.signatureUse.$ident", identity.signatureUse)
|
||||
editor.putString("$uuid.signature.$ident", identity.signature)
|
||||
editor.putString("$uuid.$IDENTITY_DESCRIPTION_KEY.$ident", identity.description)
|
||||
editor.putString("$uuid.replyTo.$ident", identity.replyTo)
|
||||
ident++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
private fun deleteIdentities(account: Account, storage: Storage, editor: StorageEditor) {
|
||||
val accountUuid = account.uuid
|
||||
|
||||
var identityIndex = 0
|
||||
var gotOne: Boolean
|
||||
do {
|
||||
gotOne = false
|
||||
val email = storage.getString("$accountUuid.$IDENTITY_EMAIL_KEY.$identityIndex", null)
|
||||
if (email != null) {
|
||||
editor.remove("$accountUuid.$IDENTITY_NAME_KEY.$identityIndex")
|
||||
editor.remove("$accountUuid.$IDENTITY_EMAIL_KEY.$identityIndex")
|
||||
editor.remove("$accountUuid.signatureUse.$identityIndex")
|
||||
editor.remove("$accountUuid.signature.$identityIndex")
|
||||
editor.remove("$accountUuid.$IDENTITY_DESCRIPTION_KEY.$identityIndex")
|
||||
editor.remove("$accountUuid.replyTo.$identityIndex")
|
||||
gotOne = true
|
||||
}
|
||||
identityIndex++
|
||||
} while (gotOne)
|
||||
}
|
||||
|
||||
fun move(editor: StorageEditor, account: Account, storage: Storage, newPosition: Int) {
|
||||
val accountUuids = storage.getString("accountUuids", "").split(",").filter { it.isNotEmpty() }
|
||||
val oldPosition = accountUuids.indexOf(account.uuid)
|
||||
if (oldPosition == -1 || oldPosition == newPosition) return
|
||||
|
||||
val newAccountUuidsString = accountUuids.toMutableList()
|
||||
.apply {
|
||||
removeAt(oldPosition)
|
||||
add(newPosition, account.uuid)
|
||||
}
|
||||
.joinToString(separator = ",")
|
||||
|
||||
editor.putString("accountUuids", newAccountUuidsString)
|
||||
}
|
||||
|
||||
private fun <T : Enum<T>> getEnumStringPref(storage: Storage, key: String, defaultEnum: T): T {
|
||||
val stringPref = storage.getString(key, null)
|
||||
|
||||
return if (stringPref == null) {
|
||||
defaultEnum
|
||||
} else {
|
||||
try {
|
||||
java.lang.Enum.valueOf<T>(defaultEnum.declaringJavaClass, stringPref)
|
||||
} catch (ex: IllegalArgumentException) {
|
||||
Timber.w(
|
||||
ex,
|
||||
"Unable to convert preference key [%s] value [%s] to enum of type %s",
|
||||
key,
|
||||
stringPref,
|
||||
defaultEnum.declaringJavaClass
|
||||
)
|
||||
|
||||
defaultEnum
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun loadDefaults(account: Account) {
|
||||
with(account) {
|
||||
localStorageProviderId = storageManager.defaultProviderId
|
||||
automaticCheckIntervalMinutes = DEFAULT_SYNC_INTERVAL
|
||||
idleRefreshMinutes = 24
|
||||
displayCount = K9.DEFAULT_VISIBLE_LIMIT
|
||||
accountNumber = UNASSIGNED_ACCOUNT_NUMBER
|
||||
isNotifyNewMail = true
|
||||
folderNotifyNewMailMode = FolderMode.ALL
|
||||
isNotifySync = false
|
||||
isNotifySelfNewMail = true
|
||||
isNotifyContactsMailOnly = false
|
||||
isIgnoreChatMessages = false
|
||||
messagesNotificationChannelVersion = 0
|
||||
folderDisplayMode = FolderMode.NOT_SECOND_CLASS
|
||||
folderSyncMode = FolderMode.FIRST_CLASS
|
||||
folderPushMode = FolderMode.NONE
|
||||
folderTargetMode = FolderMode.NOT_SECOND_CLASS
|
||||
sortType = DEFAULT_SORT_TYPE
|
||||
setSortAscending(DEFAULT_SORT_TYPE, DEFAULT_SORT_ASCENDING)
|
||||
showPictures = ShowPictures.NEVER
|
||||
isSignatureBeforeQuotedText = false
|
||||
expungePolicy = Expunge.EXPUNGE_IMMEDIATELY
|
||||
importedAutoExpandFolder = null
|
||||
legacyInboxFolder = null
|
||||
maxPushFolders = 10
|
||||
isSubscribedFoldersOnly = false
|
||||
maximumPolledMessageAge = -1
|
||||
maximumAutoDownloadMessageSize = 32768
|
||||
messageFormat = DEFAULT_MESSAGE_FORMAT
|
||||
isMessageFormatAuto = DEFAULT_MESSAGE_FORMAT_AUTO
|
||||
isMessageReadReceipt = DEFAULT_MESSAGE_READ_RECEIPT
|
||||
quoteStyle = DEFAULT_QUOTE_STYLE
|
||||
quotePrefix = DEFAULT_QUOTE_PREFIX
|
||||
isDefaultQuotedTextShown = DEFAULT_QUOTED_TEXT_SHOWN
|
||||
isReplyAfterQuote = DEFAULT_REPLY_AFTER_QUOTE
|
||||
isStripSignature = DEFAULT_STRIP_SIGNATURE
|
||||
isSyncRemoteDeletions = true
|
||||
openPgpKey = NO_OPENPGP_KEY
|
||||
isRemoteSearchFullText = false
|
||||
remoteSearchNumResults = DEFAULT_REMOTE_SEARCH_NUM_RESULTS
|
||||
isUploadSentMessages = true
|
||||
isMarkMessageAsReadOnView = true
|
||||
isMarkMessageAsReadOnDelete = true
|
||||
isAlwaysShowCcBcc = false
|
||||
lastSyncTime = 0L
|
||||
lastFolderListRefreshTime = 0L
|
||||
|
||||
setArchiveFolderId(null, SpecialFolderSelection.AUTOMATIC)
|
||||
setDraftsFolderId(null, SpecialFolderSelection.AUTOMATIC)
|
||||
setSentFolderId(null, SpecialFolderSelection.AUTOMATIC)
|
||||
setSpamFolderId(null, SpecialFolderSelection.AUTOMATIC)
|
||||
setTrashFolderId(null, SpecialFolderSelection.AUTOMATIC)
|
||||
setArchiveFolderId(null, SpecialFolderSelection.AUTOMATIC)
|
||||
|
||||
searchableFolders = Searchable.ALL
|
||||
|
||||
identities = ArrayList<Identity>()
|
||||
|
||||
val identity = Identity(
|
||||
signatureUse = false,
|
||||
signature = resourceProvider.defaultSignature(),
|
||||
description = resourceProvider.defaultIdentityDescription()
|
||||
)
|
||||
identities.add(identity)
|
||||
|
||||
updateNotificationSettings {
|
||||
NotificationSettings(
|
||||
isRingEnabled = true,
|
||||
ringtone = DEFAULT_RINGTONE_URI,
|
||||
light = NotificationLight.Disabled,
|
||||
vibration = NotificationVibration.DEFAULT
|
||||
)
|
||||
}
|
||||
|
||||
resetChangeMarkers()
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val ACCOUNT_DESCRIPTION_KEY = "description"
|
||||
const val INCOMING_SERVER_SETTINGS_KEY = "incomingServerSettings"
|
||||
const val OUTGOING_SERVER_SETTINGS_KEY = "outgoingServerSettings"
|
||||
|
||||
const val IDENTITY_NAME_KEY = "name"
|
||||
const val IDENTITY_EMAIL_KEY = "email"
|
||||
const val IDENTITY_DESCRIPTION_KEY = "description"
|
||||
|
||||
const val FALLBACK_ACCOUNT_COLOR = 0x0099CC
|
||||
|
||||
@JvmField
|
||||
val DEFAULT_MESSAGE_FORMAT = MessageFormat.HTML
|
||||
|
||||
@JvmField
|
||||
val DEFAULT_QUOTE_STYLE = QuoteStyle.PREFIX
|
||||
const val DEFAULT_MESSAGE_FORMAT_AUTO = false
|
||||
const val DEFAULT_MESSAGE_READ_RECEIPT = false
|
||||
const val DEFAULT_QUOTE_PREFIX = ">"
|
||||
const val DEFAULT_QUOTED_TEXT_SHOWN = true
|
||||
const val DEFAULT_REPLY_AFTER_QUOTE = false
|
||||
const val DEFAULT_STRIP_SIGNATURE = true
|
||||
const val DEFAULT_REMOTE_SEARCH_NUM_RESULTS = 25
|
||||
const val DEFAULT_RINGTONE_URI = "content://settings/system/notification_sound"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
package com.fsck.k9
|
||||
|
||||
fun interface AccountRemovedListener {
|
||||
fun onAccountRemoved(account: Account)
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
package com.fsck.k9;
|
||||
|
||||
|
||||
public interface AccountsChangeListener {
|
||||
void onAccountsChanged();
|
||||
}
|
||||
11
app/core/src/main/java/com/fsck/k9/ActivityExtensions.kt
Normal file
11
app/core/src/main/java/com/fsck/k9/ActivityExtensions.kt
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
package com.fsck.k9
|
||||
|
||||
import android.app.Activity
|
||||
import android.widget.Toast
|
||||
import androidx.annotation.StringRes
|
||||
|
||||
fun Activity.finishWithErrorToast(@StringRes errorRes: Int, vararg formatArgs: String) {
|
||||
val text = getString(errorRes, *formatArgs)
|
||||
Toast.makeText(this, text, Toast.LENGTH_LONG).show()
|
||||
finish()
|
||||
}
|
||||
5
app/core/src/main/java/com/fsck/k9/AppConfig.kt
Normal file
5
app/core/src/main/java/com/fsck/k9/AppConfig.kt
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
package com.fsck.k9
|
||||
|
||||
data class AppConfig(
|
||||
val componentsToDisable: List<Class<*>>
|
||||
)
|
||||
7
app/core/src/main/java/com/fsck/k9/BaseAccount.kt
Normal file
7
app/core/src/main/java/com/fsck/k9/BaseAccount.kt
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
package com.fsck.k9
|
||||
|
||||
interface BaseAccount {
|
||||
val uuid: String
|
||||
val name: String?
|
||||
val email: String
|
||||
}
|
||||
89
app/core/src/main/java/com/fsck/k9/Core.kt
Normal file
89
app/core/src/main/java/com/fsck/k9/Core.kt
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
package com.fsck.k9
|
||||
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageManager
|
||||
import com.fsck.k9.job.K9JobManager
|
||||
import com.fsck.k9.mail.internet.BinaryTempFileBody
|
||||
import com.fsck.k9.notification.NotificationController
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import org.koin.core.qualifier.named
|
||||
|
||||
object Core : EarlyInit {
|
||||
private val context: Context by inject()
|
||||
private val appConfig: AppConfig by inject()
|
||||
private val jobManager: K9JobManager by inject()
|
||||
private val appCoroutineScope: CoroutineScope by inject(named("AppCoroutineScope"))
|
||||
private val preferences: Preferences by inject()
|
||||
private val notificationController: NotificationController by inject()
|
||||
|
||||
/**
|
||||
* This needs to be called from [Application#onCreate][android.app.Application#onCreate] before calling through
|
||||
* to the super class's `onCreate` implementation and before initializing the dependency injection library.
|
||||
*/
|
||||
fun earlyInit() {
|
||||
if (K9.DEVELOPER_MODE) {
|
||||
enableStrictMode()
|
||||
}
|
||||
}
|
||||
|
||||
fun init(context: Context) {
|
||||
BinaryTempFileBody.setTempDirectory(context.cacheDir)
|
||||
|
||||
setServicesEnabled(context)
|
||||
|
||||
restoreNotifications()
|
||||
}
|
||||
|
||||
/**
|
||||
* Called throughout the application when the number of accounts has changed. This method
|
||||
* enables or disables the Compose activity, the boot receiver and the service based on
|
||||
* whether any accounts are configured.
|
||||
*/
|
||||
@JvmStatic
|
||||
fun setServicesEnabled(context: Context) {
|
||||
val appContext = context.applicationContext
|
||||
val acctLength = Preferences.getPreferences().accounts.size
|
||||
val enable = acctLength > 0
|
||||
|
||||
setServicesEnabled(appContext, enable)
|
||||
}
|
||||
|
||||
fun setServicesEnabled() {
|
||||
setServicesEnabled(context)
|
||||
}
|
||||
|
||||
private fun setServicesEnabled(context: Context, enabled: Boolean) {
|
||||
val pm = context.packageManager
|
||||
|
||||
for (clazz in appConfig.componentsToDisable) {
|
||||
val alreadyEnabled = pm.getComponentEnabledSetting(ComponentName(context, clazz)) ==
|
||||
PackageManager.COMPONENT_ENABLED_STATE_ENABLED
|
||||
|
||||
if (enabled != alreadyEnabled) {
|
||||
pm.setComponentEnabledSetting(
|
||||
ComponentName(context, clazz),
|
||||
if (enabled) {
|
||||
PackageManager.COMPONENT_ENABLED_STATE_ENABLED
|
||||
} else {
|
||||
PackageManager.COMPONENT_ENABLED_STATE_DISABLED
|
||||
},
|
||||
PackageManager.DONT_KILL_APP
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (enabled) {
|
||||
jobManager.scheduleAllMailJobs()
|
||||
}
|
||||
}
|
||||
|
||||
private fun restoreNotifications() {
|
||||
appCoroutineScope.launch(Dispatchers.IO) {
|
||||
val accounts = preferences.accounts
|
||||
notificationController.restoreNewMailNotifications(accounts)
|
||||
}
|
||||
}
|
||||
}
|
||||
36
app/core/src/main/java/com/fsck/k9/CoreKoinModules.kt
Normal file
36
app/core/src/main/java/com/fsck/k9/CoreKoinModules.kt
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
package com.fsck.k9
|
||||
|
||||
import com.fsck.k9.autocrypt.autocryptModule
|
||||
import com.fsck.k9.controller.controllerModule
|
||||
import com.fsck.k9.controller.push.controllerPushModule
|
||||
import com.fsck.k9.crypto.openPgpModule
|
||||
import com.fsck.k9.helper.helperModule
|
||||
import com.fsck.k9.job.jobModule
|
||||
import com.fsck.k9.logging.loggingModule
|
||||
import com.fsck.k9.mailstore.mailStoreModule
|
||||
import com.fsck.k9.message.extractors.extractorModule
|
||||
import com.fsck.k9.message.html.htmlModule
|
||||
import com.fsck.k9.message.quote.quoteModule
|
||||
import com.fsck.k9.network.connectivityModule
|
||||
import com.fsck.k9.notification.coreNotificationModule
|
||||
import com.fsck.k9.power.powerModule
|
||||
import com.fsck.k9.preferences.preferencesModule
|
||||
|
||||
val coreModules = listOf(
|
||||
mainModule,
|
||||
openPgpModule,
|
||||
autocryptModule,
|
||||
mailStoreModule,
|
||||
extractorModule,
|
||||
htmlModule,
|
||||
quoteModule,
|
||||
coreNotificationModule,
|
||||
controllerModule,
|
||||
controllerPushModule,
|
||||
jobModule,
|
||||
helperModule,
|
||||
preferencesModule,
|
||||
connectivityModule,
|
||||
powerModule,
|
||||
loggingModule
|
||||
)
|
||||
35
app/core/src/main/java/com/fsck/k9/CoreResourceProvider.kt
Normal file
35
app/core/src/main/java/com/fsck/k9/CoreResourceProvider.kt
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
package com.fsck.k9
|
||||
|
||||
import com.fsck.k9.notification.PushNotificationState
|
||||
|
||||
interface CoreResourceProvider {
|
||||
fun defaultSignature(): String
|
||||
fun defaultIdentityDescription(): String
|
||||
|
||||
fun contactDisplayNamePrefix(): String
|
||||
fun contactUnknownSender(): String
|
||||
fun contactUnknownRecipient(): String
|
||||
|
||||
fun messageHeaderFrom(): String
|
||||
fun messageHeaderTo(): String
|
||||
fun messageHeaderCc(): String
|
||||
fun messageHeaderDate(): String
|
||||
fun messageHeaderSubject(): String
|
||||
fun messageHeaderSeparator(): String
|
||||
|
||||
fun noSubject(): String
|
||||
fun userAgent(): String
|
||||
|
||||
fun encryptedSubject(): String
|
||||
fun replyHeader(sender: String): String
|
||||
fun replyHeader(sender: String, sentDate: String): String
|
||||
|
||||
fun searchUnifiedInboxTitle(): String
|
||||
fun searchUnifiedInboxDetail(): String
|
||||
|
||||
fun outboxFolderName(): String
|
||||
|
||||
val iconPushNotification: Int
|
||||
fun pushNotificationText(notificationState: PushNotificationState): String
|
||||
fun pushNotificationInfoText(): String
|
||||
}
|
||||
44
app/core/src/main/java/com/fsck/k9/DI.kt
Normal file
44
app/core/src/main/java/com/fsck/k9/DI.kt
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
package com.fsck.k9
|
||||
|
||||
import android.app.Application
|
||||
import com.fsck.k9.core.BuildConfig
|
||||
import org.koin.android.ext.koin.androidContext
|
||||
import org.koin.android.ext.koin.androidLogger
|
||||
import org.koin.core.context.startKoin
|
||||
import org.koin.core.module.Module
|
||||
import org.koin.core.parameter.ParametersDefinition
|
||||
import org.koin.core.qualifier.Qualifier
|
||||
import org.koin.java.KoinJavaComponent.getKoin
|
||||
import org.koin.java.KoinJavaComponent.get as koinGet
|
||||
|
||||
object DI {
|
||||
private const val DEBUG = false
|
||||
|
||||
@JvmStatic fun start(application: Application, modules: List<Module>) {
|
||||
startKoin {
|
||||
if (BuildConfig.DEBUG && DEBUG) {
|
||||
androidLogger()
|
||||
}
|
||||
|
||||
androidContext(application)
|
||||
modules(modules)
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun <T : Any> get(clazz: Class<T>): T {
|
||||
return koinGet(clazz)
|
||||
}
|
||||
|
||||
inline fun <reified T : Any> get(): T {
|
||||
return koinGet(T::class.java)
|
||||
}
|
||||
}
|
||||
|
||||
interface EarlyInit
|
||||
|
||||
// Copied from ComponentCallbacks.inject()
|
||||
inline fun <reified T : Any> EarlyInit.inject(
|
||||
qualifier: Qualifier? = null,
|
||||
noinline parameters: ParametersDefinition? = null
|
||||
) = lazy { getKoin().get<T>(qualifier, parameters) }
|
||||
27
app/core/src/main/java/com/fsck/k9/EmailAddressValidator.kt
Normal file
27
app/core/src/main/java/com/fsck/k9/EmailAddressValidator.kt
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
package com.fsck.k9
|
||||
|
||||
import java.util.regex.Pattern
|
||||
|
||||
class EmailAddressValidator {
|
||||
|
||||
fun isValidAddressOnly(text: CharSequence): Boolean = EMAIL_ADDRESS_PATTERN.matcher(text).matches()
|
||||
|
||||
companion object {
|
||||
|
||||
// https://www.rfc-editor.org/rfc/rfc2396.txt (3.2.2)
|
||||
// https://www.rfc-editor.org/rfc/rfc5321.txt (4.1.2)
|
||||
|
||||
private const val ALPHA = "[a-zA-Z]"
|
||||
private const val ALPHANUM = "[a-zA-Z0-9]"
|
||||
private const val ATEXT = "[0-9a-zA-Z!#$%&'*+\\-/=?^_`{|}~]"
|
||||
private const val QCONTENT = "([\\p{Graph}\\p{Blank}&&[^\"\\\\]]|\\\\[\\p{Graph}\\p{Blank}])"
|
||||
private const val TOP_LABEL = "(($ALPHA($ALPHANUM|\\-|_)*$ALPHANUM)|$ALPHA)"
|
||||
private const val DOMAIN_LABEL = "(($ALPHANUM($ALPHANUM|\\-|_)*$ALPHANUM)|$ALPHANUM)"
|
||||
private const val HOST_NAME = "((($DOMAIN_LABEL\\.)+$TOP_LABEL)|$DOMAIN_LABEL)"
|
||||
|
||||
private val EMAIL_ADDRESS_PATTERN = Pattern.compile(
|
||||
"^($ATEXT+(\\.$ATEXT+)*|\"$QCONTENT+\")" +
|
||||
"\\@$HOST_NAME"
|
||||
)
|
||||
}
|
||||
}
|
||||
197
app/core/src/main/java/com/fsck/k9/FontSizes.java
Normal file
197
app/core/src/main/java/com/fsck/k9/FontSizes.java
Normal file
|
|
@ -0,0 +1,197 @@
|
|||
package com.fsck.k9;
|
||||
|
||||
import android.util.TypedValue;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.fsck.k9.preferences.Storage;
|
||||
import com.fsck.k9.preferences.StorageEditor;
|
||||
|
||||
|
||||
/**
|
||||
* Manage font size of the information displayed in the message list and in the message view.
|
||||
*/
|
||||
public class FontSizes {
|
||||
private static final String MESSAGE_LIST_SUBJECT = "fontSizeMessageListSubject";
|
||||
private static final String MESSAGE_LIST_SENDER = "fontSizeMessageListSender";
|
||||
private static final String MESSAGE_LIST_DATE = "fontSizeMessageListDate";
|
||||
private static final String MESSAGE_LIST_PREVIEW = "fontSizeMessageListPreview";
|
||||
private static final String MESSAGE_VIEW_ACCOUNT_NAME = "fontSizeMessageViewAccountName";
|
||||
private static final String MESSAGE_VIEW_SENDER = "fontSizeMessageViewSender";
|
||||
private static final String MESSAGE_VIEW_RECIPIENTS = "fontSizeMessageViewTo";
|
||||
private static final String MESSAGE_VIEW_SUBJECT = "fontSizeMessageViewSubject";
|
||||
private static final String MESSAGE_VIEW_DATE = "fontSizeMessageViewDate";
|
||||
private static final String MESSAGE_VIEW_CONTENT_PERCENT = "fontSizeMessageViewContentPercent";
|
||||
private static final String MESSAGE_COMPOSE_INPUT = "fontSizeMessageComposeInput";
|
||||
|
||||
public static final int FONT_DEFAULT = -1; // Don't force-reset the size of this setting
|
||||
public static final int FONT_10SP = 10;
|
||||
public static final int FONT_12SP = 12;
|
||||
public static final int SMALL = 14; // ?android:attr/textAppearanceSmall
|
||||
public static final int FONT_16SP = 16;
|
||||
public static final int MEDIUM = 18; // ?android:attr/textAppearanceMedium
|
||||
public static final int FONT_20SP = 20;
|
||||
public static final int LARGE = 22; // ?android:attr/textAppearanceLarge
|
||||
|
||||
|
||||
private int messageListSubject;
|
||||
private int messageListSender;
|
||||
private int messageListDate;
|
||||
private int messageListPreview;
|
||||
private int messageViewAccountName;
|
||||
private int messageViewSender;
|
||||
private int messageViewRecipients;
|
||||
private int messageViewSubject;
|
||||
private int messageViewDate;
|
||||
private int messageViewContentPercent;
|
||||
private int messageComposeInput;
|
||||
|
||||
|
||||
public FontSizes() {
|
||||
messageListSubject = FONT_DEFAULT;
|
||||
messageListSender = FONT_DEFAULT;
|
||||
messageListDate = FONT_DEFAULT;
|
||||
messageListPreview = FONT_DEFAULT;
|
||||
|
||||
messageViewAccountName = FONT_DEFAULT;
|
||||
messageViewSender = FONT_DEFAULT;
|
||||
messageViewRecipients = FONT_DEFAULT;
|
||||
messageViewSubject = FONT_DEFAULT;
|
||||
messageViewDate = FONT_DEFAULT;
|
||||
messageViewContentPercent = 100;
|
||||
|
||||
messageComposeInput = MEDIUM;
|
||||
}
|
||||
|
||||
public void save(StorageEditor editor) {
|
||||
editor.putInt(MESSAGE_LIST_SUBJECT, messageListSubject);
|
||||
editor.putInt(MESSAGE_LIST_SENDER, messageListSender);
|
||||
editor.putInt(MESSAGE_LIST_DATE, messageListDate);
|
||||
editor.putInt(MESSAGE_LIST_PREVIEW, messageListPreview);
|
||||
|
||||
editor.putInt(MESSAGE_VIEW_ACCOUNT_NAME, messageViewAccountName);
|
||||
editor.putInt(MESSAGE_VIEW_SENDER, messageViewSender);
|
||||
editor.putInt(MESSAGE_VIEW_RECIPIENTS, messageViewRecipients);
|
||||
editor.putInt(MESSAGE_VIEW_SUBJECT, messageViewSubject);
|
||||
editor.putInt(MESSAGE_VIEW_DATE, messageViewDate);
|
||||
editor.putInt(MESSAGE_VIEW_CONTENT_PERCENT, getMessageViewContentAsPercent());
|
||||
|
||||
editor.putInt(MESSAGE_COMPOSE_INPUT, messageComposeInput);
|
||||
}
|
||||
|
||||
public void load(Storage storage) {
|
||||
messageListSubject = storage.getInt(MESSAGE_LIST_SUBJECT, messageListSubject);
|
||||
messageListSender = storage.getInt(MESSAGE_LIST_SENDER, messageListSender);
|
||||
messageListDate = storage.getInt(MESSAGE_LIST_DATE, messageListDate);
|
||||
messageListPreview = storage.getInt(MESSAGE_LIST_PREVIEW, messageListPreview);
|
||||
|
||||
messageViewAccountName = storage.getInt(MESSAGE_VIEW_ACCOUNT_NAME, messageViewAccountName);
|
||||
messageViewSender = storage.getInt(MESSAGE_VIEW_SENDER, messageViewSender);
|
||||
messageViewRecipients = storage.getInt(MESSAGE_VIEW_RECIPIENTS, messageViewRecipients);
|
||||
messageViewSubject = storage.getInt(MESSAGE_VIEW_SUBJECT, messageViewSubject);
|
||||
messageViewDate = storage.getInt(MESSAGE_VIEW_DATE, messageViewDate);
|
||||
|
||||
loadMessageViewContentPercent(storage);
|
||||
|
||||
messageComposeInput = storage.getInt(MESSAGE_COMPOSE_INPUT, messageComposeInput);
|
||||
}
|
||||
|
||||
private void loadMessageViewContentPercent(Storage storage) {
|
||||
setMessageViewContentAsPercent(storage.getInt(MESSAGE_VIEW_CONTENT_PERCENT, 100));
|
||||
}
|
||||
|
||||
public int getMessageListSubject() {
|
||||
return messageListSubject;
|
||||
}
|
||||
|
||||
public void setMessageListSubject(int messageListSubject) {
|
||||
this.messageListSubject = messageListSubject;
|
||||
}
|
||||
|
||||
public int getMessageListSender() {
|
||||
return messageListSender;
|
||||
}
|
||||
|
||||
public void setMessageListSender(int messageListSender) {
|
||||
this.messageListSender = messageListSender;
|
||||
}
|
||||
|
||||
public int getMessageListDate() {
|
||||
return messageListDate;
|
||||
}
|
||||
|
||||
public void setMessageListDate(int messageListDate) {
|
||||
this.messageListDate = messageListDate;
|
||||
}
|
||||
|
||||
public int getMessageListPreview() {
|
||||
return messageListPreview;
|
||||
}
|
||||
|
||||
public void setMessageListPreview(int messageListPreview) {
|
||||
this.messageListPreview = messageListPreview;
|
||||
}
|
||||
|
||||
public int getMessageViewAccountName() {
|
||||
return messageViewAccountName;
|
||||
}
|
||||
|
||||
public void setMessageViewAccountName(int messageViewAccountName) {
|
||||
this.messageViewAccountName = messageViewAccountName;
|
||||
}
|
||||
|
||||
public int getMessageViewSender() {
|
||||
return messageViewSender;
|
||||
}
|
||||
|
||||
public void setMessageViewSender(int messageViewSender) {
|
||||
this.messageViewSender = messageViewSender;
|
||||
}
|
||||
|
||||
public int getMessageViewRecipients() {
|
||||
return messageViewRecipients;
|
||||
}
|
||||
|
||||
public void setMessageViewRecipients(int messageViewRecipients) {
|
||||
this.messageViewRecipients = messageViewRecipients;
|
||||
}
|
||||
|
||||
public int getMessageViewSubject() {
|
||||
return messageViewSubject;
|
||||
}
|
||||
|
||||
public void setMessageViewSubject(int messageViewSubject) {
|
||||
this.messageViewSubject = messageViewSubject;
|
||||
}
|
||||
|
||||
public int getMessageViewDate() {
|
||||
return messageViewDate;
|
||||
}
|
||||
|
||||
public void setMessageViewDate(int messageViewDate) {
|
||||
this.messageViewDate = messageViewDate;
|
||||
}
|
||||
|
||||
public int getMessageViewContentAsPercent() {
|
||||
return messageViewContentPercent;
|
||||
}
|
||||
|
||||
public void setMessageViewContentAsPercent(int size) {
|
||||
messageViewContentPercent = size;
|
||||
}
|
||||
|
||||
public int getMessageComposeInput() {
|
||||
return messageComposeInput;
|
||||
}
|
||||
|
||||
public void setMessageComposeInput(int messageComposeInput) {
|
||||
this.messageComposeInput = messageComposeInput;
|
||||
}
|
||||
|
||||
// This, arguably, should live somewhere in a view class, but since we call it from activities, fragments
|
||||
// and views, where isn't exactly clear.
|
||||
public void setViewTextSize(TextView v, int fontSize) {
|
||||
if (fontSize != FONT_DEFAULT) {
|
||||
v.setTextSize(TypedValue.COMPLEX_UNIT_SP, fontSize);
|
||||
}
|
||||
}
|
||||
}
|
||||
20
app/core/src/main/java/com/fsck/k9/Identity.kt
Normal file
20
app/core/src/main/java/com/fsck/k9/Identity.kt
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
package com.fsck.k9
|
||||
|
||||
import android.os.Parcelable
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@Parcelize
|
||||
data class Identity(
|
||||
val description: String? = null,
|
||||
val name: String? = null,
|
||||
val email: String? = null,
|
||||
val signature: String? = null,
|
||||
val signatureUse: Boolean = false,
|
||||
val replyTo: String? = null
|
||||
) : Parcelable {
|
||||
// TODO remove when callers are converted to Kotlin
|
||||
fun withName(name: String?) = copy(name = name)
|
||||
fun withSignature(signature: String?) = copy(signature = signature)
|
||||
fun withSignatureUse(signatureUse: Boolean) = copy(signatureUse = signatureUse)
|
||||
fun withEmail(email: String?) = copy(email = email)
|
||||
}
|
||||
535
app/core/src/main/java/com/fsck/k9/K9.kt
Normal file
535
app/core/src/main/java/com/fsck/k9/K9.kt
Normal file
|
|
@ -0,0 +1,535 @@
|
|||
package com.fsck.k9
|
||||
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import com.fsck.k9.Account.SortType
|
||||
import com.fsck.k9.core.BuildConfig
|
||||
import com.fsck.k9.mail.K9MailLib
|
||||
import com.fsck.k9.mailstore.LocalStore
|
||||
import com.fsck.k9.preferences.RealGeneralSettingsManager
|
||||
import com.fsck.k9.preferences.Storage
|
||||
import com.fsck.k9.preferences.StorageEditor
|
||||
import kotlinx.datetime.Clock
|
||||
import timber.log.Timber
|
||||
import timber.log.Timber.DebugTree
|
||||
|
||||
@Deprecated("Use GeneralSettingsManager and GeneralSettings instead")
|
||||
object K9 : EarlyInit {
|
||||
private val generalSettingsManager: RealGeneralSettingsManager by inject()
|
||||
|
||||
/**
|
||||
* If this is `true`, various development settings will be enabled.
|
||||
*/
|
||||
@JvmField
|
||||
val DEVELOPER_MODE = BuildConfig.DEBUG
|
||||
|
||||
/**
|
||||
* Name of the [SharedPreferences] file used to store the last known version of the
|
||||
* accounts' databases.
|
||||
*
|
||||
* See `UpgradeDatabases` for a detailed explanation of the database upgrade process.
|
||||
*/
|
||||
private const val DATABASE_VERSION_CACHE = "database_version_cache"
|
||||
|
||||
/**
|
||||
* Key used to store the last known database version of the accounts' databases.
|
||||
*
|
||||
* @see DATABASE_VERSION_CACHE
|
||||
*/
|
||||
private const val KEY_LAST_ACCOUNT_DATABASE_VERSION = "last_account_database_version"
|
||||
|
||||
/**
|
||||
* A reference to the [SharedPreferences] used for caching the last known database version.
|
||||
*
|
||||
* @see checkCachedDatabaseVersion
|
||||
* @see setDatabasesUpToDate
|
||||
*/
|
||||
private var databaseVersionCache: SharedPreferences? = null
|
||||
|
||||
/**
|
||||
* @see areDatabasesUpToDate
|
||||
*/
|
||||
private var databasesUpToDate = false
|
||||
|
||||
/**
|
||||
* Check if we already know whether all databases are using the current database schema.
|
||||
*
|
||||
* This method is only used for optimizations. If it returns `true` we can be certain that getting a [LocalStore]
|
||||
* instance won't trigger a schema upgrade.
|
||||
*
|
||||
* @return `true`, if we know that all databases are using the current database schema. `false`, otherwise.
|
||||
*/
|
||||
@Synchronized
|
||||
@JvmStatic
|
||||
fun areDatabasesUpToDate(): Boolean {
|
||||
return databasesUpToDate
|
||||
}
|
||||
|
||||
/**
|
||||
* Remember that all account databases are using the most recent database schema.
|
||||
*
|
||||
* @param save
|
||||
* Whether or not to write the current database version to the
|
||||
* `SharedPreferences` [.DATABASE_VERSION_CACHE].
|
||||
*
|
||||
* @see .areDatabasesUpToDate
|
||||
*/
|
||||
@Synchronized
|
||||
@JvmStatic
|
||||
fun setDatabasesUpToDate(save: Boolean) {
|
||||
databasesUpToDate = true
|
||||
|
||||
if (save) {
|
||||
val editor = databaseVersionCache!!.edit()
|
||||
editor.putInt(KEY_LAST_ACCOUNT_DATABASE_VERSION, LocalStore.getDbVersion())
|
||||
editor.apply()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the last known database version of the accounts' databases from a `SharedPreference`.
|
||||
*
|
||||
* If the stored version matches [LocalStore.getDbVersion] we know that the databases are up to date.
|
||||
* Using `SharedPreferences` should be a lot faster than opening all SQLite databases to get the current database
|
||||
* version.
|
||||
*
|
||||
* See the class `UpgradeDatabases` for a detailed explanation of the database upgrade process.
|
||||
*
|
||||
* @see areDatabasesUpToDate
|
||||
*/
|
||||
private fun checkCachedDatabaseVersion(context: Context) {
|
||||
databaseVersionCache = context.getSharedPreferences(DATABASE_VERSION_CACHE, Context.MODE_PRIVATE)
|
||||
|
||||
val cachedVersion = databaseVersionCache!!.getInt(KEY_LAST_ACCOUNT_DATABASE_VERSION, 0)
|
||||
|
||||
if (cachedVersion >= LocalStore.getDbVersion()) {
|
||||
setDatabasesUpToDate(false)
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
var isDebugLoggingEnabled: Boolean = DEVELOPER_MODE
|
||||
set(debug) {
|
||||
field = debug
|
||||
updateLoggingStatus()
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
var isSensitiveDebugLoggingEnabled: Boolean = false
|
||||
|
||||
@JvmStatic
|
||||
var k9Language = ""
|
||||
|
||||
@JvmStatic
|
||||
val fontSizes = FontSizes()
|
||||
|
||||
@JvmStatic
|
||||
var backgroundOps = BACKGROUND_OPS.ALWAYS
|
||||
|
||||
@JvmStatic
|
||||
var isShowAnimations = true
|
||||
|
||||
@JvmStatic
|
||||
var isConfirmDelete = false
|
||||
|
||||
@JvmStatic
|
||||
var isConfirmDiscardMessage = true
|
||||
|
||||
@JvmStatic
|
||||
var isConfirmDeleteStarred = false
|
||||
|
||||
@JvmStatic
|
||||
var isConfirmSpam = false
|
||||
|
||||
@JvmStatic
|
||||
var isConfirmDeleteFromNotification = true
|
||||
|
||||
@JvmStatic
|
||||
var isConfirmMarkAllRead = true
|
||||
|
||||
@JvmStatic
|
||||
var notificationQuickDeleteBehaviour = NotificationQuickDelete.ALWAYS
|
||||
|
||||
@JvmStatic
|
||||
var lockScreenNotificationVisibility = LockScreenNotificationVisibility.MESSAGE_COUNT
|
||||
|
||||
@JvmStatic
|
||||
var messageListDensity: UiDensity = UiDensity.Default
|
||||
|
||||
@JvmStatic
|
||||
var isShowMessageListStars = true
|
||||
|
||||
@JvmStatic
|
||||
var messageListPreviewLines = 2
|
||||
|
||||
@JvmStatic
|
||||
var isShowCorrespondentNames = true
|
||||
|
||||
@JvmStatic
|
||||
var isMessageListSenderAboveSubject = false
|
||||
|
||||
@JvmStatic
|
||||
var isShowContactName = false
|
||||
|
||||
@JvmStatic
|
||||
var isChangeContactNameColor = false
|
||||
|
||||
@JvmStatic
|
||||
var contactNameColor = 0xFF1093F5.toInt()
|
||||
|
||||
@JvmStatic
|
||||
var isShowContactPicture = true
|
||||
|
||||
@JvmStatic
|
||||
var isUseMessageViewFixedWidthFont = false
|
||||
|
||||
@JvmStatic
|
||||
var isMessageViewReturnToList = false
|
||||
|
||||
@JvmStatic
|
||||
var isMessageViewShowNext = false
|
||||
|
||||
@JvmStatic
|
||||
var isUseVolumeKeysForNavigation = false
|
||||
|
||||
@JvmStatic
|
||||
var isShowUnifiedInbox = true
|
||||
|
||||
@JvmStatic
|
||||
var isShowStarredCount = false
|
||||
|
||||
@JvmStatic
|
||||
var isAutoFitWidth: Boolean = false
|
||||
|
||||
var isQuietTimeEnabled = false
|
||||
var isNotificationDuringQuietTimeEnabled = true
|
||||
var quietTimeStarts: String? = null
|
||||
var quietTimeEnds: String? = null
|
||||
|
||||
@JvmStatic
|
||||
var isHideUserAgent = false
|
||||
|
||||
@JvmStatic
|
||||
var isHideTimeZone = false
|
||||
|
||||
@get:Synchronized
|
||||
@set:Synchronized
|
||||
@JvmStatic
|
||||
var sortType: SortType = Account.DEFAULT_SORT_TYPE
|
||||
private val sortAscending = mutableMapOf<SortType, Boolean>()
|
||||
|
||||
@JvmStatic
|
||||
var isUseBackgroundAsUnreadIndicator = false
|
||||
|
||||
@get:Synchronized
|
||||
@set:Synchronized
|
||||
var isShowComposeButtonOnMessageList = true
|
||||
|
||||
@get:Synchronized
|
||||
@set:Synchronized
|
||||
@JvmStatic
|
||||
var isThreadedViewEnabled = true
|
||||
|
||||
@get:Synchronized
|
||||
@set:Synchronized
|
||||
@JvmStatic
|
||||
var splitViewMode = SplitViewMode.NEVER
|
||||
|
||||
var isColorizeMissingContactPictures = true
|
||||
|
||||
@JvmStatic
|
||||
var isMessageViewArchiveActionVisible = false
|
||||
|
||||
@JvmStatic
|
||||
var isMessageViewDeleteActionVisible = true
|
||||
|
||||
@JvmStatic
|
||||
var isMessageViewMoveActionVisible = false
|
||||
|
||||
@JvmStatic
|
||||
var isMessageViewCopyActionVisible = false
|
||||
|
||||
@JvmStatic
|
||||
var isMessageViewSpamActionVisible = false
|
||||
|
||||
@JvmStatic
|
||||
var pgpInlineDialogCounter: Int = 0
|
||||
|
||||
@JvmStatic
|
||||
var pgpSignOnlyDialogCounter: Int = 0
|
||||
|
||||
@JvmStatic
|
||||
var swipeRightAction: SwipeAction = SwipeAction.ToggleSelection
|
||||
|
||||
@JvmStatic
|
||||
var swipeLeftAction: SwipeAction = SwipeAction.ToggleRead
|
||||
|
||||
val isQuietTime: Boolean
|
||||
get() {
|
||||
if (!isQuietTimeEnabled) {
|
||||
return false
|
||||
}
|
||||
|
||||
val clock = DI.get<Clock>()
|
||||
val quietTimeChecker = QuietTimeChecker(clock, quietTimeStarts, quietTimeEnds)
|
||||
return quietTimeChecker.isQuietTime
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
@JvmStatic
|
||||
fun isSortAscending(sortType: SortType): Boolean {
|
||||
if (sortAscending[sortType] == null) {
|
||||
sortAscending[sortType] = sortType.isDefaultAscending
|
||||
}
|
||||
return sortAscending[sortType]!!
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
@JvmStatic
|
||||
fun setSortAscending(sortType: SortType, sortAscending: Boolean) {
|
||||
K9.sortAscending[sortType] = sortAscending
|
||||
}
|
||||
|
||||
fun init(context: Context) {
|
||||
K9MailLib.setDebugStatus(object : K9MailLib.DebugStatus {
|
||||
override fun enabled(): Boolean = isDebugLoggingEnabled
|
||||
|
||||
override fun debugSensitive(): Boolean = isSensitiveDebugLoggingEnabled
|
||||
})
|
||||
com.fsck.k9.logging.Timber.logger = TimberLogger()
|
||||
|
||||
checkCachedDatabaseVersion(context)
|
||||
|
||||
loadPrefs(generalSettingsManager.storage)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun loadPrefs(storage: Storage) {
|
||||
isDebugLoggingEnabled = storage.getBoolean("enableDebugLogging", DEVELOPER_MODE)
|
||||
isSensitiveDebugLoggingEnabled = storage.getBoolean("enableSensitiveLogging", false)
|
||||
isShowAnimations = storage.getBoolean("animations", true)
|
||||
isUseVolumeKeysForNavigation = storage.getBoolean("useVolumeKeysForNavigation", false)
|
||||
isShowUnifiedInbox = storage.getBoolean("showUnifiedInbox", true)
|
||||
isShowStarredCount = storage.getBoolean("showStarredCount", false)
|
||||
isMessageListSenderAboveSubject = storage.getBoolean("messageListSenderAboveSubject", false)
|
||||
isShowMessageListStars = storage.getBoolean("messageListStars", true)
|
||||
messageListPreviewLines = storage.getInt("messageListPreviewLines", 2)
|
||||
|
||||
isAutoFitWidth = storage.getBoolean("autofitWidth", true)
|
||||
|
||||
isQuietTimeEnabled = storage.getBoolean("quietTimeEnabled", false)
|
||||
isNotificationDuringQuietTimeEnabled = storage.getBoolean("notificationDuringQuietTimeEnabled", true)
|
||||
quietTimeStarts = storage.getString("quietTimeStarts", "21:00")
|
||||
quietTimeEnds = storage.getString("quietTimeEnds", "7:00")
|
||||
|
||||
messageListDensity = storage.getEnum("messageListDensity", UiDensity.Default)
|
||||
isShowCorrespondentNames = storage.getBoolean("showCorrespondentNames", true)
|
||||
isShowContactName = storage.getBoolean("showContactName", false)
|
||||
isShowContactPicture = storage.getBoolean("showContactPicture", true)
|
||||
isChangeContactNameColor = storage.getBoolean("changeRegisteredNameColor", false)
|
||||
contactNameColor = storage.getInt("registeredNameColor", 0xFF1093F5.toInt())
|
||||
isUseMessageViewFixedWidthFont = storage.getBoolean("messageViewFixedWidthFont", false)
|
||||
isMessageViewReturnToList = storage.getBoolean("messageViewReturnToList", false)
|
||||
isMessageViewShowNext = storage.getBoolean("messageViewShowNext", false)
|
||||
isHideUserAgent = storage.getBoolean("hideUserAgent", false)
|
||||
isHideTimeZone = storage.getBoolean("hideTimeZone", false)
|
||||
|
||||
isConfirmDelete = storage.getBoolean("confirmDelete", false)
|
||||
isConfirmDiscardMessage = storage.getBoolean("confirmDiscardMessage", true)
|
||||
isConfirmDeleteStarred = storage.getBoolean("confirmDeleteStarred", false)
|
||||
isConfirmSpam = storage.getBoolean("confirmSpam", false)
|
||||
isConfirmDeleteFromNotification = storage.getBoolean("confirmDeleteFromNotification", true)
|
||||
isConfirmMarkAllRead = storage.getBoolean("confirmMarkAllRead", true)
|
||||
|
||||
sortType = storage.getEnum("sortTypeEnum", Account.DEFAULT_SORT_TYPE)
|
||||
|
||||
val sortAscendingSetting = storage.getBoolean("sortAscending", Account.DEFAULT_SORT_ASCENDING)
|
||||
sortAscending[sortType] = sortAscendingSetting
|
||||
|
||||
notificationQuickDeleteBehaviour = storage.getEnum("notificationQuickDelete", NotificationQuickDelete.ALWAYS)
|
||||
|
||||
lockScreenNotificationVisibility = storage.getEnum(
|
||||
"lockScreenNotificationVisibility",
|
||||
LockScreenNotificationVisibility.MESSAGE_COUNT
|
||||
)
|
||||
|
||||
splitViewMode = storage.getEnum("splitViewMode", SplitViewMode.NEVER)
|
||||
|
||||
isUseBackgroundAsUnreadIndicator = storage.getBoolean("useBackgroundAsUnreadIndicator", false)
|
||||
isShowComposeButtonOnMessageList = storage.getBoolean("showComposeButtonOnMessageList", true)
|
||||
isThreadedViewEnabled = storage.getBoolean("threadedView", true)
|
||||
fontSizes.load(storage)
|
||||
|
||||
backgroundOps = storage.getEnum("backgroundOperations", BACKGROUND_OPS.ALWAYS)
|
||||
|
||||
isColorizeMissingContactPictures = storage.getBoolean("colorizeMissingContactPictures", true)
|
||||
|
||||
isMessageViewArchiveActionVisible = storage.getBoolean("messageViewArchiveActionVisible", false)
|
||||
isMessageViewDeleteActionVisible = storage.getBoolean("messageViewDeleteActionVisible", true)
|
||||
isMessageViewMoveActionVisible = storage.getBoolean("messageViewMoveActionVisible", false)
|
||||
isMessageViewCopyActionVisible = storage.getBoolean("messageViewCopyActionVisible", false)
|
||||
isMessageViewSpamActionVisible = storage.getBoolean("messageViewSpamActionVisible", false)
|
||||
|
||||
pgpInlineDialogCounter = storage.getInt("pgpInlineDialogCounter", 0)
|
||||
pgpSignOnlyDialogCounter = storage.getInt("pgpSignOnlyDialogCounter", 0)
|
||||
|
||||
k9Language = storage.getString("language", "")
|
||||
|
||||
swipeRightAction = storage.getEnum("swipeRightAction", SwipeAction.ToggleSelection)
|
||||
swipeLeftAction = storage.getEnum("swipeLeftAction", SwipeAction.ToggleRead)
|
||||
}
|
||||
|
||||
internal fun save(editor: StorageEditor) {
|
||||
editor.putBoolean("enableDebugLogging", isDebugLoggingEnabled)
|
||||
editor.putBoolean("enableSensitiveLogging", isSensitiveDebugLoggingEnabled)
|
||||
editor.putEnum("backgroundOperations", backgroundOps)
|
||||
editor.putBoolean("animations", isShowAnimations)
|
||||
editor.putBoolean("useVolumeKeysForNavigation", isUseVolumeKeysForNavigation)
|
||||
editor.putBoolean("autofitWidth", isAutoFitWidth)
|
||||
editor.putBoolean("quietTimeEnabled", isQuietTimeEnabled)
|
||||
editor.putBoolean("notificationDuringQuietTimeEnabled", isNotificationDuringQuietTimeEnabled)
|
||||
editor.putString("quietTimeStarts", quietTimeStarts)
|
||||
editor.putString("quietTimeEnds", quietTimeEnds)
|
||||
|
||||
editor.putEnum("messageListDensity", messageListDensity)
|
||||
editor.putBoolean("messageListSenderAboveSubject", isMessageListSenderAboveSubject)
|
||||
editor.putBoolean("showUnifiedInbox", isShowUnifiedInbox)
|
||||
editor.putBoolean("showStarredCount", isShowStarredCount)
|
||||
editor.putBoolean("messageListStars", isShowMessageListStars)
|
||||
editor.putInt("messageListPreviewLines", messageListPreviewLines)
|
||||
editor.putBoolean("showCorrespondentNames", isShowCorrespondentNames)
|
||||
editor.putBoolean("showContactName", isShowContactName)
|
||||
editor.putBoolean("showContactPicture", isShowContactPicture)
|
||||
editor.putBoolean("changeRegisteredNameColor", isChangeContactNameColor)
|
||||
editor.putInt("registeredNameColor", contactNameColor)
|
||||
editor.putBoolean("messageViewFixedWidthFont", isUseMessageViewFixedWidthFont)
|
||||
editor.putBoolean("messageViewReturnToList", isMessageViewReturnToList)
|
||||
editor.putBoolean("messageViewShowNext", isMessageViewShowNext)
|
||||
editor.putBoolean("hideUserAgent", isHideUserAgent)
|
||||
editor.putBoolean("hideTimeZone", isHideTimeZone)
|
||||
|
||||
editor.putString("language", k9Language)
|
||||
|
||||
editor.putBoolean("confirmDelete", isConfirmDelete)
|
||||
editor.putBoolean("confirmDiscardMessage", isConfirmDiscardMessage)
|
||||
editor.putBoolean("confirmDeleteStarred", isConfirmDeleteStarred)
|
||||
editor.putBoolean("confirmSpam", isConfirmSpam)
|
||||
editor.putBoolean("confirmDeleteFromNotification", isConfirmDeleteFromNotification)
|
||||
editor.putBoolean("confirmMarkAllRead", isConfirmMarkAllRead)
|
||||
|
||||
editor.putEnum("sortTypeEnum", sortType)
|
||||
editor.putBoolean("sortAscending", sortAscending[sortType] ?: false)
|
||||
|
||||
editor.putString("notificationQuickDelete", notificationQuickDeleteBehaviour.toString())
|
||||
editor.putString("lockScreenNotificationVisibility", lockScreenNotificationVisibility.toString())
|
||||
|
||||
editor.putBoolean("useBackgroundAsUnreadIndicator", isUseBackgroundAsUnreadIndicator)
|
||||
editor.putBoolean("showComposeButtonOnMessageList", isShowComposeButtonOnMessageList)
|
||||
editor.putBoolean("threadedView", isThreadedViewEnabled)
|
||||
editor.putEnum("splitViewMode", splitViewMode)
|
||||
editor.putBoolean("colorizeMissingContactPictures", isColorizeMissingContactPictures)
|
||||
|
||||
editor.putBoolean("messageViewArchiveActionVisible", isMessageViewArchiveActionVisible)
|
||||
editor.putBoolean("messageViewDeleteActionVisible", isMessageViewDeleteActionVisible)
|
||||
editor.putBoolean("messageViewMoveActionVisible", isMessageViewMoveActionVisible)
|
||||
editor.putBoolean("messageViewCopyActionVisible", isMessageViewCopyActionVisible)
|
||||
editor.putBoolean("messageViewSpamActionVisible", isMessageViewSpamActionVisible)
|
||||
|
||||
editor.putInt("pgpInlineDialogCounter", pgpInlineDialogCounter)
|
||||
editor.putInt("pgpSignOnlyDialogCounter", pgpSignOnlyDialogCounter)
|
||||
|
||||
editor.putEnum("swipeRightAction", swipeRightAction)
|
||||
editor.putEnum("swipeLeftAction", swipeLeftAction)
|
||||
|
||||
fontSizes.save(editor)
|
||||
}
|
||||
|
||||
private fun updateLoggingStatus() {
|
||||
Timber.uprootAll()
|
||||
if (isDebugLoggingEnabled) {
|
||||
Timber.plant(DebugTree())
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun saveSettingsAsync() {
|
||||
generalSettingsManager.saveSettingsAsync()
|
||||
}
|
||||
|
||||
private inline fun <reified T : Enum<T>> Storage.getEnum(key: String, defaultValue: T): T {
|
||||
return try {
|
||||
val value = getString(key, null)
|
||||
if (value != null) {
|
||||
enumValueOf(value)
|
||||
} else {
|
||||
defaultValue
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Timber.e("Couldn't read setting '%s'. Using default value instead.", key)
|
||||
defaultValue
|
||||
}
|
||||
}
|
||||
|
||||
private fun <T : Enum<T>> StorageEditor.putEnum(key: String, value: T) {
|
||||
putString(key, value.name)
|
||||
}
|
||||
|
||||
const val LOCAL_UID_PREFIX = "K9LOCAL:"
|
||||
|
||||
const val IDENTITY_HEADER = K9MailLib.IDENTITY_HEADER
|
||||
|
||||
/**
|
||||
* Specifies how many messages will be shown in a folder by default. This number is set
|
||||
* on each new folder and can be incremented with "Load more messages..." by the
|
||||
* VISIBLE_LIMIT_INCREMENT
|
||||
*/
|
||||
const val DEFAULT_VISIBLE_LIMIT = 25
|
||||
|
||||
/**
|
||||
* The maximum size of an attachment we're willing to download (either View or Save)
|
||||
* Attachments that are base64 encoded (most) will be about 1.375x their actual size
|
||||
* so we should probably factor that in. A 5MB attachment will generally be around
|
||||
* 6.8MB downloaded but only 5MB saved.
|
||||
*/
|
||||
const val MAX_ATTACHMENT_DOWNLOAD_SIZE = 128 * 1024 * 1024
|
||||
|
||||
/**
|
||||
* How many times should K-9 try to deliver a message before giving up until the app is killed and restarted
|
||||
*/
|
||||
const val MAX_SEND_ATTEMPTS = 5
|
||||
|
||||
const val MANUAL_WAKE_LOCK_TIMEOUT = 120000
|
||||
const val PUSH_WAKE_LOCK_TIMEOUT = K9MailLib.PUSH_WAKE_LOCK_TIMEOUT
|
||||
const val MAIL_SERVICE_WAKE_LOCK_TIMEOUT = 60000
|
||||
const val BOOT_RECEIVER_WAKE_LOCK_TIMEOUT = 60000
|
||||
|
||||
enum class BACKGROUND_OPS {
|
||||
ALWAYS, NEVER, WHEN_CHECKED_AUTO_SYNC
|
||||
}
|
||||
|
||||
/**
|
||||
* Controls behaviour of delete button in notifications.
|
||||
*/
|
||||
enum class NotificationQuickDelete {
|
||||
ALWAYS,
|
||||
FOR_SINGLE_MSG,
|
||||
NEVER
|
||||
}
|
||||
|
||||
enum class LockScreenNotificationVisibility {
|
||||
EVERYTHING,
|
||||
SENDERS,
|
||||
MESSAGE_COUNT,
|
||||
APP_NAME,
|
||||
NOTHING
|
||||
}
|
||||
|
||||
/**
|
||||
* Controls when to use the message list split view.
|
||||
*/
|
||||
enum class SplitViewMode {
|
||||
ALWAYS,
|
||||
NEVER,
|
||||
WHEN_IN_LANDSCAPE
|
||||
}
|
||||
}
|
||||
40
app/core/src/main/java/com/fsck/k9/KoinModule.kt
Normal file
40
app/core/src/main/java/com/fsck/k9/KoinModule.kt
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
package com.fsck.k9
|
||||
|
||||
import android.content.Context
|
||||
import app.k9mail.core.android.common.coreCommonAndroidModule
|
||||
import com.fsck.k9.helper.Contacts
|
||||
import com.fsck.k9.helper.DefaultTrustedSocketFactory
|
||||
import com.fsck.k9.mail.ssl.LocalKeyStore
|
||||
import com.fsck.k9.mail.ssl.TrustManagerFactory
|
||||
import com.fsck.k9.mail.ssl.TrustedSocketFactory
|
||||
import com.fsck.k9.mailstore.LocalStoreProvider
|
||||
import com.fsck.k9.setup.ServerNameSuggester
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.datetime.Clock
|
||||
import org.koin.core.qualifier.named
|
||||
import org.koin.dsl.module
|
||||
|
||||
val mainModule = module {
|
||||
includes(coreCommonAndroidModule)
|
||||
single<CoroutineScope>(named("AppCoroutineScope")) { GlobalScope }
|
||||
single {
|
||||
Preferences(
|
||||
storagePersister = get(),
|
||||
localStoreProvider = get(),
|
||||
accountPreferenceSerializer = get()
|
||||
)
|
||||
}
|
||||
single { get<Context>().resources }
|
||||
single { get<Context>().contentResolver }
|
||||
single { LocalStoreProvider() }
|
||||
single { Contacts() }
|
||||
single { LocalKeyStore(directoryProvider = get()) }
|
||||
single { TrustManagerFactory.createInstance(get()) }
|
||||
single { LocalKeyStoreManager(get()) }
|
||||
single<TrustedSocketFactory> { DefaultTrustedSocketFactory(get(), get()) }
|
||||
single<Clock> { Clock.System }
|
||||
factory { ServerNameSuggester() }
|
||||
factory { EmailAddressValidator() }
|
||||
factory { ServerSettingsSerializer() }
|
||||
}
|
||||
59
app/core/src/main/java/com/fsck/k9/LocalKeyStoreManager.kt
Normal file
59
app/core/src/main/java/com/fsck/k9/LocalKeyStoreManager.kt
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
package com.fsck.k9
|
||||
|
||||
import com.fsck.k9.mail.MailServerDirection
|
||||
import com.fsck.k9.mail.ssl.LocalKeyStore
|
||||
import java.security.cert.CertificateException
|
||||
import java.security.cert.X509Certificate
|
||||
|
||||
class LocalKeyStoreManager(
|
||||
private val localKeyStore: LocalKeyStore
|
||||
) {
|
||||
/**
|
||||
* Add a new certificate for the incoming or outgoing server to the local key store.
|
||||
*/
|
||||
@Throws(CertificateException::class)
|
||||
fun addCertificate(account: Account, direction: MailServerDirection, certificate: X509Certificate) {
|
||||
val serverSettings = if (direction === MailServerDirection.INCOMING) {
|
||||
account.incomingServerSettings
|
||||
} else {
|
||||
account.outgoingServerSettings
|
||||
}
|
||||
localKeyStore.addCertificate(serverSettings.host!!, serverSettings.port, certificate)
|
||||
}
|
||||
|
||||
/**
|
||||
* Examine the existing settings for an account. If the old host/port is different from the
|
||||
* new host/port, then try and delete any (possibly non-existent) certificate stored for the
|
||||
* old host/port.
|
||||
*/
|
||||
fun deleteCertificate(account: Account, newHost: String, newPort: Int, direction: MailServerDirection) {
|
||||
val serverSettings = if (direction === MailServerDirection.INCOMING) {
|
||||
account.incomingServerSettings
|
||||
} else {
|
||||
account.outgoingServerSettings
|
||||
}
|
||||
val oldHost = serverSettings.host!!
|
||||
val oldPort = serverSettings.port
|
||||
if (oldPort == -1) {
|
||||
// This occurs when a new account is created
|
||||
return
|
||||
}
|
||||
if (newHost != oldHost || newPort != oldPort) {
|
||||
localKeyStore.deleteCertificate(oldHost, oldPort)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Examine the settings for the account and attempt to delete (possibly non-existent)
|
||||
* certificates for the incoming and outgoing servers.
|
||||
*/
|
||||
fun deleteCertificates(account: Account) {
|
||||
account.incomingServerSettings.let { serverSettings ->
|
||||
localKeyStore.deleteCertificate(serverSettings.host!!, serverSettings.port)
|
||||
}
|
||||
|
||||
account.outgoingServerSettings.let { serverSettings ->
|
||||
localKeyStore.deleteCertificate(serverSettings.host!!, serverSettings.port)
|
||||
}
|
||||
}
|
||||
}
|
||||
33
app/core/src/main/java/com/fsck/k9/NotificationLight.kt
Normal file
33
app/core/src/main/java/com/fsck/k9/NotificationLight.kt
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
package com.fsck.k9
|
||||
|
||||
import android.app.Notification
|
||||
|
||||
enum class NotificationLight {
|
||||
Disabled,
|
||||
AccountColor,
|
||||
SystemDefaultColor,
|
||||
White,
|
||||
Red,
|
||||
Green,
|
||||
Blue,
|
||||
Yellow,
|
||||
Cyan,
|
||||
Magenta;
|
||||
|
||||
fun toColor(account: Account): Int? {
|
||||
return when (this) {
|
||||
Disabled -> null
|
||||
AccountColor -> account.chipColor.toArgb()
|
||||
SystemDefaultColor -> Notification.COLOR_DEFAULT
|
||||
White -> 0xFFFFFF.toArgb()
|
||||
Red -> 0xFF0000.toArgb()
|
||||
Green -> 0x00FF00.toArgb()
|
||||
Blue -> 0x0000FF.toArgb()
|
||||
Yellow -> 0xFFFF00.toArgb()
|
||||
Cyan -> 0x00FFFF.toArgb()
|
||||
Magenta -> 0xFF00FF.toArgb()
|
||||
}
|
||||
}
|
||||
|
||||
private fun Int.toArgb() = this or 0xFF000000L.toInt()
|
||||
}
|
||||
11
app/core/src/main/java/com/fsck/k9/NotificationSettings.kt
Normal file
11
app/core/src/main/java/com/fsck/k9/NotificationSettings.kt
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
package com.fsck.k9
|
||||
|
||||
/**
|
||||
* Describes how a notification should behave.
|
||||
*/
|
||||
data class NotificationSettings(
|
||||
val isRingEnabled: Boolean = false,
|
||||
val ringtone: String? = null,
|
||||
val light: NotificationLight = NotificationLight.Disabled,
|
||||
val vibration: NotificationVibration = NotificationVibration.DEFAULT
|
||||
)
|
||||
62
app/core/src/main/java/com/fsck/k9/NotificationVibration.kt
Normal file
62
app/core/src/main/java/com/fsck/k9/NotificationVibration.kt
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
package com.fsck.k9
|
||||
|
||||
data class NotificationVibration(
|
||||
val isEnabled: Boolean,
|
||||
val pattern: VibratePattern,
|
||||
val repeatCount: Int
|
||||
) {
|
||||
val systemPattern: LongArray
|
||||
get() = getSystemPattern(pattern, repeatCount)
|
||||
|
||||
companion object {
|
||||
val DEFAULT = NotificationVibration(isEnabled = false, pattern = VibratePattern.Default, repeatCount = 5)
|
||||
|
||||
fun getSystemPattern(vibratePattern: VibratePattern, repeatCount: Int): LongArray {
|
||||
val selectedPattern = vibratePattern.vibrationPattern
|
||||
val repeatedPattern = LongArray(selectedPattern.size * repeatCount)
|
||||
for (n in 0 until repeatCount) {
|
||||
System.arraycopy(selectedPattern, 0, repeatedPattern, n * selectedPattern.size, selectedPattern.size)
|
||||
}
|
||||
|
||||
// Do not wait before starting the vibration pattern.
|
||||
repeatedPattern[0] = 0
|
||||
|
||||
return repeatedPattern
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum class VibratePattern(
|
||||
/**
|
||||
* These are "off, on" patterns, specified in milliseconds.
|
||||
*/
|
||||
val vibrationPattern: LongArray
|
||||
) {
|
||||
Default(vibrationPattern = longArrayOf(300, 200)),
|
||||
Pattern1(vibrationPattern = longArrayOf(100, 200)),
|
||||
Pattern2(vibrationPattern = longArrayOf(100, 500)),
|
||||
Pattern3(vibrationPattern = longArrayOf(200, 200)),
|
||||
Pattern4(vibrationPattern = longArrayOf(200, 500)),
|
||||
Pattern5(vibrationPattern = longArrayOf(500, 500));
|
||||
|
||||
fun serialize(): Int = when (this) {
|
||||
Default -> 0
|
||||
Pattern1 -> 1
|
||||
Pattern2 -> 2
|
||||
Pattern3 -> 3
|
||||
Pattern4 -> 4
|
||||
Pattern5 -> 5
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun deserialize(value: Int): VibratePattern = when (value) {
|
||||
0 -> Default
|
||||
1 -> Pattern1
|
||||
2 -> Pattern2
|
||||
3 -> Pattern3
|
||||
4 -> Pattern4
|
||||
5 -> Pattern5
|
||||
else -> error("Unknown VibratePattern value: $value")
|
||||
}
|
||||
}
|
||||
}
|
||||
300
app/core/src/main/java/com/fsck/k9/Preferences.kt
Normal file
300
app/core/src/main/java/com/fsck/k9/Preferences.kt
Normal file
|
|
@ -0,0 +1,300 @@
|
|||
package com.fsck.k9
|
||||
|
||||
import androidx.annotation.GuardedBy
|
||||
import androidx.annotation.RestrictTo
|
||||
import com.fsck.k9.mail.MessagingException
|
||||
import com.fsck.k9.mailstore.LocalStoreProvider
|
||||
import com.fsck.k9.preferences.AccountManager
|
||||
import com.fsck.k9.preferences.Storage
|
||||
import com.fsck.k9.preferences.StorageEditor
|
||||
import com.fsck.k9.preferences.StoragePersister
|
||||
import java.util.HashMap
|
||||
import java.util.LinkedList
|
||||
import java.util.UUID
|
||||
import java.util.concurrent.CopyOnWriteArraySet
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.channels.awaitClose
|
||||
import kotlinx.coroutines.channels.trySendBlocking
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.buffer
|
||||
import kotlinx.coroutines.flow.callbackFlow
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import timber.log.Timber
|
||||
|
||||
class Preferences internal constructor(
|
||||
private val storagePersister: StoragePersister,
|
||||
private val localStoreProvider: LocalStoreProvider,
|
||||
private val accountPreferenceSerializer: AccountPreferenceSerializer,
|
||||
private val backgroundDispatcher: CoroutineDispatcher = Dispatchers.IO
|
||||
) : AccountManager {
|
||||
private val accountLock = Any()
|
||||
private val storageLock = Any()
|
||||
|
||||
@GuardedBy("accountLock")
|
||||
private var accountsMap: MutableMap<String, Account>? = null
|
||||
|
||||
@GuardedBy("accountLock")
|
||||
private var accountsInOrder = mutableListOf<Account>()
|
||||
|
||||
@GuardedBy("accountLock")
|
||||
private var newAccount: Account? = null
|
||||
private val accountsChangeListeners = CopyOnWriteArraySet<AccountsChangeListener>()
|
||||
private val accountRemovedListeners = CopyOnWriteArraySet<AccountRemovedListener>()
|
||||
|
||||
@GuardedBy("storageLock")
|
||||
private var currentStorage: Storage? = null
|
||||
|
||||
val storage: Storage
|
||||
get() = synchronized(storageLock) {
|
||||
currentStorage ?: storagePersister.loadValues().also { newStorage ->
|
||||
currentStorage = newStorage
|
||||
}
|
||||
}
|
||||
|
||||
fun createStorageEditor(): StorageEditor {
|
||||
return storagePersister.createStorageEditor { updater ->
|
||||
synchronized(storageLock) {
|
||||
currentStorage = updater(storage)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@RestrictTo(RestrictTo.Scope.TESTS)
|
||||
fun clearAccounts() {
|
||||
synchronized(accountLock) {
|
||||
accountsMap = HashMap()
|
||||
accountsInOrder = LinkedList()
|
||||
}
|
||||
}
|
||||
|
||||
fun loadAccounts() {
|
||||
synchronized(accountLock) {
|
||||
val accounts = mutableMapOf<String, Account>()
|
||||
val accountsInOrder = mutableListOf<Account>()
|
||||
|
||||
val accountUuids = storage.getString("accountUuids", null)
|
||||
if (!accountUuids.isNullOrEmpty()) {
|
||||
accountUuids.split(",").forEach { uuid ->
|
||||
val newAccount = Account(uuid)
|
||||
accountPreferenceSerializer.loadAccount(newAccount, storage)
|
||||
|
||||
accounts[uuid] = newAccount
|
||||
accountsInOrder.add(newAccount)
|
||||
}
|
||||
}
|
||||
|
||||
newAccount?.takeIf { it.accountNumber != -1 }?.let { newAccount ->
|
||||
accounts[newAccount.uuid] = newAccount
|
||||
if (newAccount !in accountsInOrder) {
|
||||
accountsInOrder.add(newAccount)
|
||||
}
|
||||
this.newAccount = null
|
||||
}
|
||||
|
||||
this.accountsMap = accounts
|
||||
this.accountsInOrder = accountsInOrder
|
||||
}
|
||||
}
|
||||
|
||||
val accounts: List<Account>
|
||||
get() {
|
||||
synchronized(accountLock) {
|
||||
if (accountsMap == null) {
|
||||
loadAccounts()
|
||||
}
|
||||
|
||||
return accountsInOrder.toList()
|
||||
}
|
||||
}
|
||||
|
||||
private val completeAccounts: List<Account>
|
||||
get() = accounts.filter { it.isFinishedSetup }
|
||||
|
||||
override fun getAccount(accountUuid: String): Account? {
|
||||
synchronized(accountLock) {
|
||||
if (accountsMap == null) {
|
||||
loadAccounts()
|
||||
}
|
||||
|
||||
return accountsMap!![accountUuid]
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
override fun getAccountFlow(accountUuid: String): Flow<Account> {
|
||||
return callbackFlow {
|
||||
val initialAccount = getAccount(accountUuid)
|
||||
if (initialAccount == null) {
|
||||
close()
|
||||
return@callbackFlow
|
||||
}
|
||||
|
||||
send(initialAccount)
|
||||
|
||||
val listener = AccountsChangeListener {
|
||||
val account = getAccount(accountUuid)
|
||||
if (account != null) {
|
||||
trySendBlocking(account)
|
||||
} else {
|
||||
close()
|
||||
}
|
||||
}
|
||||
addOnAccountsChangeListener(listener)
|
||||
|
||||
awaitClose {
|
||||
removeOnAccountsChangeListener(listener)
|
||||
}
|
||||
}.buffer(capacity = Channel.CONFLATED)
|
||||
.flowOn(backgroundDispatcher)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
override fun getAccountsFlow(): Flow<List<Account>> {
|
||||
return callbackFlow {
|
||||
send(completeAccounts)
|
||||
|
||||
val listener = AccountsChangeListener {
|
||||
trySendBlocking(completeAccounts)
|
||||
}
|
||||
addOnAccountsChangeListener(listener)
|
||||
|
||||
awaitClose {
|
||||
removeOnAccountsChangeListener(listener)
|
||||
}
|
||||
}.buffer(capacity = Channel.CONFLATED)
|
||||
.flowOn(backgroundDispatcher)
|
||||
}
|
||||
|
||||
fun newAccount(): Account {
|
||||
val accountUuid = UUID.randomUUID().toString()
|
||||
val account = Account(accountUuid)
|
||||
accountPreferenceSerializer.loadDefaults(account)
|
||||
|
||||
synchronized(accountLock) {
|
||||
newAccount = account
|
||||
accountsMap!![account.uuid] = account
|
||||
accountsInOrder.add(account)
|
||||
}
|
||||
|
||||
return account
|
||||
}
|
||||
|
||||
fun deleteAccount(account: Account) {
|
||||
synchronized(accountLock) {
|
||||
accountsMap?.remove(account.uuid)
|
||||
accountsInOrder.remove(account)
|
||||
|
||||
val storageEditor = createStorageEditor()
|
||||
accountPreferenceSerializer.delete(storageEditor, storage, account)
|
||||
storageEditor.commit()
|
||||
|
||||
if (account === newAccount) {
|
||||
newAccount = null
|
||||
}
|
||||
}
|
||||
|
||||
notifyAccountRemovedListeners(account)
|
||||
notifyAccountsChangeListeners()
|
||||
}
|
||||
|
||||
val defaultAccount: Account?
|
||||
get() = accounts.firstOrNull()
|
||||
|
||||
override fun saveAccount(account: Account) {
|
||||
ensureAssignedAccountNumber(account)
|
||||
processChangedValues(account)
|
||||
|
||||
synchronized(accountLock) {
|
||||
val editor = createStorageEditor()
|
||||
accountPreferenceSerializer.save(editor, storage, account)
|
||||
editor.commit()
|
||||
}
|
||||
|
||||
notifyAccountsChangeListeners()
|
||||
}
|
||||
|
||||
private fun ensureAssignedAccountNumber(account: Account) {
|
||||
if (account.accountNumber != Account.UNASSIGNED_ACCOUNT_NUMBER) return
|
||||
|
||||
account.accountNumber = generateAccountNumber()
|
||||
}
|
||||
|
||||
private fun processChangedValues(account: Account) {
|
||||
if (account.isChangedVisibleLimits) {
|
||||
try {
|
||||
localStoreProvider.getInstance(account).resetVisibleLimits(account.displayCount)
|
||||
} catch (e: MessagingException) {
|
||||
Timber.e(e, "Failed to load LocalStore!")
|
||||
}
|
||||
}
|
||||
account.resetChangeMarkers()
|
||||
}
|
||||
|
||||
fun generateAccountNumber(): Int {
|
||||
val accountNumbers = accounts.map { it.accountNumber }
|
||||
return findNewAccountNumber(accountNumbers)
|
||||
}
|
||||
|
||||
private fun findNewAccountNumber(accountNumbers: List<Int>): Int {
|
||||
var newAccountNumber = -1
|
||||
for (accountNumber in accountNumbers.sorted()) {
|
||||
if (accountNumber > newAccountNumber + 1) {
|
||||
break
|
||||
}
|
||||
newAccountNumber = accountNumber
|
||||
}
|
||||
newAccountNumber++
|
||||
|
||||
return newAccountNumber
|
||||
}
|
||||
|
||||
override fun moveAccount(account: Account, newPosition: Int) {
|
||||
synchronized(accountLock) {
|
||||
val storageEditor = createStorageEditor()
|
||||
accountPreferenceSerializer.move(storageEditor, account, storage, newPosition)
|
||||
storageEditor.commit()
|
||||
|
||||
loadAccounts()
|
||||
}
|
||||
|
||||
notifyAccountsChangeListeners()
|
||||
}
|
||||
|
||||
private fun notifyAccountsChangeListeners() {
|
||||
for (listener in accountsChangeListeners) {
|
||||
listener.onAccountsChanged()
|
||||
}
|
||||
}
|
||||
|
||||
override fun addOnAccountsChangeListener(accountsChangeListener: AccountsChangeListener) {
|
||||
accountsChangeListeners.add(accountsChangeListener)
|
||||
}
|
||||
|
||||
override fun removeOnAccountsChangeListener(accountsChangeListener: AccountsChangeListener) {
|
||||
accountsChangeListeners.remove(accountsChangeListener)
|
||||
}
|
||||
|
||||
private fun notifyAccountRemovedListeners(account: Account) {
|
||||
for (listener in accountRemovedListeners) {
|
||||
listener.onAccountRemoved(account)
|
||||
}
|
||||
}
|
||||
|
||||
override fun addAccountRemovedListener(listener: AccountRemovedListener) {
|
||||
accountRemovedListeners.add(listener)
|
||||
}
|
||||
|
||||
fun removeAccountRemovedListener(listener: AccountRemovedListener) {
|
||||
accountRemovedListeners.remove(listener)
|
||||
}
|
||||
|
||||
companion object {
|
||||
@JvmStatic
|
||||
fun getPreferences(): Preferences {
|
||||
return DI.get()
|
||||
}
|
||||
}
|
||||
}
|
||||
46
app/core/src/main/java/com/fsck/k9/QuietTimeChecker.java
Normal file
46
app/core/src/main/java/com/fsck/k9/QuietTimeChecker.java
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
package com.fsck.k9;
|
||||
|
||||
|
||||
import java.util.Calendar;
|
||||
|
||||
import kotlinx.datetime.Clock;
|
||||
|
||||
|
||||
class QuietTimeChecker {
|
||||
private final Clock clock;
|
||||
private final int quietTimeStart;
|
||||
private final int quietTimeEnd;
|
||||
|
||||
|
||||
QuietTimeChecker(Clock clock, String quietTimeStart, String quietTimeEnd) {
|
||||
this.clock = clock;
|
||||
this.quietTimeStart = parseTime(quietTimeStart);
|
||||
this.quietTimeEnd = parseTime(quietTimeEnd);
|
||||
}
|
||||
|
||||
private static int parseTime(String time) {
|
||||
String[] parts = time.split(":");
|
||||
int hour = Integer.parseInt(parts[0]);
|
||||
int minute = Integer.parseInt(parts[1]);
|
||||
|
||||
return hour * 60 + minute;
|
||||
}
|
||||
|
||||
public boolean isQuietTime() {
|
||||
// If start and end times are the same, we're never quiet
|
||||
if (quietTimeStart == quietTimeEnd) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Calendar calendar = Calendar.getInstance();
|
||||
calendar.setTimeInMillis(clock.now().toEpochMilliseconds());
|
||||
|
||||
int minutesSinceMidnight = (calendar.get(Calendar.HOUR_OF_DAY) * 60) + calendar.get(Calendar.MINUTE);
|
||||
|
||||
if (quietTimeStart > quietTimeEnd) {
|
||||
return minutesSinceMidnight >= quietTimeStart || minutesSinceMidnight <= quietTimeEnd;
|
||||
} else {
|
||||
return minutesSinceMidnight >= quietTimeStart && minutesSinceMidnight <= quietTimeEnd;
|
||||
}
|
||||
}
|
||||
}
|
||||
122
app/core/src/main/java/com/fsck/k9/ServerSettingsSerializer.kt
Normal file
122
app/core/src/main/java/com/fsck/k9/ServerSettingsSerializer.kt
Normal file
|
|
@ -0,0 +1,122 @@
|
|||
package com.fsck.k9
|
||||
|
||||
import com.fsck.k9.mail.AuthType
|
||||
import com.fsck.k9.mail.ConnectionSecurity
|
||||
import com.fsck.k9.mail.ServerSettings
|
||||
import com.squareup.moshi.JsonAdapter
|
||||
import com.squareup.moshi.JsonReader
|
||||
import com.squareup.moshi.JsonReader.Token
|
||||
import com.squareup.moshi.JsonWriter
|
||||
|
||||
class ServerSettingsSerializer {
|
||||
private val adapter = ServerSettingsAdapter()
|
||||
|
||||
fun serialize(serverSettings: ServerSettings): String {
|
||||
return adapter.toJson(serverSettings)
|
||||
}
|
||||
|
||||
fun deserialize(json: String): ServerSettings {
|
||||
return adapter.fromJson(json)!!
|
||||
}
|
||||
}
|
||||
|
||||
private const val KEY_TYPE = "type"
|
||||
private const val KEY_HOST = "host"
|
||||
private const val KEY_PORT = "port"
|
||||
private const val KEY_CONNECTION_SECURITY = "connectionSecurity"
|
||||
private const val KEY_AUTHENTICATION_TYPE = "authenticationType"
|
||||
private const val KEY_USERNAME = "username"
|
||||
private const val KEY_PASSWORD = "password"
|
||||
private const val KEY_CLIENT_CERTIFICATE_ALIAS = "clientCertificateAlias"
|
||||
|
||||
private val JSON_KEYS = JsonReader.Options.of(
|
||||
KEY_TYPE,
|
||||
KEY_HOST,
|
||||
KEY_PORT,
|
||||
KEY_CONNECTION_SECURITY,
|
||||
KEY_AUTHENTICATION_TYPE,
|
||||
KEY_USERNAME,
|
||||
KEY_PASSWORD,
|
||||
KEY_CLIENT_CERTIFICATE_ALIAS
|
||||
)
|
||||
|
||||
private class ServerSettingsAdapter : JsonAdapter<ServerSettings>() {
|
||||
override fun fromJson(reader: JsonReader): ServerSettings {
|
||||
reader.beginObject()
|
||||
|
||||
var type: String? = null
|
||||
var host: String? = null
|
||||
var port: Int? = null
|
||||
var connectionSecurity: ConnectionSecurity? = null
|
||||
var authenticationType: AuthType? = null
|
||||
var username: String? = null
|
||||
var password: String? = null
|
||||
var clientCertificateAlias: String? = null
|
||||
val extra = mutableMapOf<String, String?>()
|
||||
|
||||
while (reader.hasNext()) {
|
||||
when (reader.selectName(JSON_KEYS)) {
|
||||
0 -> type = reader.nextString()
|
||||
1 -> host = reader.nextString()
|
||||
2 -> port = reader.nextInt()
|
||||
3 -> connectionSecurity = ConnectionSecurity.valueOf(reader.nextString())
|
||||
4 -> authenticationType = AuthType.valueOf(reader.nextString())
|
||||
5 -> username = reader.nextString()
|
||||
6 -> password = reader.nextStringOrNull()
|
||||
7 -> clientCertificateAlias = reader.nextStringOrNull()
|
||||
else -> {
|
||||
val key = reader.nextName()
|
||||
val value = reader.nextStringOrNull()
|
||||
extra[key] = value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
reader.endObject()
|
||||
|
||||
requireNotNull(type) { "'type' must not be missing" }
|
||||
requireNotNull(host) { "'host' must not be missing" }
|
||||
requireNotNull(port) { "'port' must not be missing" }
|
||||
requireNotNull(connectionSecurity) { "'connectionSecurity' must not be missing" }
|
||||
requireNotNull(authenticationType) { "'authenticationType' must not be missing" }
|
||||
requireNotNull(username) { "'username' must not be missing" }
|
||||
|
||||
return ServerSettings(
|
||||
type,
|
||||
host,
|
||||
port,
|
||||
connectionSecurity,
|
||||
authenticationType,
|
||||
username,
|
||||
password,
|
||||
clientCertificateAlias,
|
||||
extra
|
||||
)
|
||||
}
|
||||
|
||||
override fun toJson(writer: JsonWriter, serverSettings: ServerSettings?) {
|
||||
requireNotNull(serverSettings)
|
||||
|
||||
writer.beginObject()
|
||||
writer.serializeNulls = true
|
||||
|
||||
writer.name(KEY_TYPE).value(serverSettings.type)
|
||||
writer.name(KEY_HOST).value(serverSettings.host)
|
||||
writer.name(KEY_PORT).value(serverSettings.port)
|
||||
writer.name(KEY_CONNECTION_SECURITY).value(serverSettings.connectionSecurity.name)
|
||||
writer.name(KEY_AUTHENTICATION_TYPE).value(serverSettings.authenticationType.name)
|
||||
writer.name(KEY_USERNAME).value(serverSettings.username)
|
||||
writer.name(KEY_PASSWORD).value(serverSettings.password)
|
||||
writer.name(KEY_CLIENT_CERTIFICATE_ALIAS).value(serverSettings.clientCertificateAlias)
|
||||
|
||||
for ((key, value) in serverSettings.extra) {
|
||||
writer.name(key).value(value)
|
||||
}
|
||||
|
||||
writer.endObject()
|
||||
}
|
||||
|
||||
private fun JsonReader.nextStringOrNull(): String? {
|
||||
return if (peek() == Token.NULL) nextNull() else nextString()
|
||||
}
|
||||
}
|
||||
44
app/core/src/main/java/com/fsck/k9/StrictMode.kt
Normal file
44
app/core/src/main/java/com/fsck/k9/StrictMode.kt
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
package com.fsck.k9
|
||||
|
||||
import android.os.Build
|
||||
import android.os.StrictMode
|
||||
import android.os.StrictMode.ThreadPolicy
|
||||
import android.os.StrictMode.VmPolicy
|
||||
|
||||
fun enableStrictMode() {
|
||||
StrictMode.setThreadPolicy(createThreadPolicy())
|
||||
StrictMode.setVmPolicy(createVmPolicy())
|
||||
}
|
||||
|
||||
private fun createThreadPolicy(): ThreadPolicy {
|
||||
return ThreadPolicy.Builder()
|
||||
.detectAll()
|
||||
.penaltyLog()
|
||||
.build()
|
||||
}
|
||||
|
||||
private fun createVmPolicy(): VmPolicy {
|
||||
return VmPolicy.Builder()
|
||||
.detectActivityLeaks()
|
||||
.detectLeakedClosableObjects()
|
||||
.detectLeakedRegistrationObjects()
|
||||
.detectFileUriExposure()
|
||||
.detectLeakedSqlLiteObjects()
|
||||
.apply {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
detectContentUriWithoutPermission()
|
||||
|
||||
// Disabled because we currently don't use tagged sockets; so this would generate a lot of noise
|
||||
// detectUntaggedSockets()
|
||||
}
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
detectCredentialProtectedWhileLocked()
|
||||
}
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
detectIncorrectContextUse()
|
||||
detectUnsafeIntentLaunch()
|
||||
}
|
||||
}
|
||||
.penaltyLog()
|
||||
.build()
|
||||
}
|
||||
12
app/core/src/main/java/com/fsck/k9/SwipeAction.kt
Normal file
12
app/core/src/main/java/com/fsck/k9/SwipeAction.kt
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
package com.fsck.k9
|
||||
|
||||
enum class SwipeAction(val removesItem: Boolean) {
|
||||
None(removesItem = false),
|
||||
ToggleSelection(removesItem = false),
|
||||
ToggleRead(removesItem = false),
|
||||
ToggleStar(removesItem = false),
|
||||
Archive(removesItem = true),
|
||||
Delete(removesItem = true),
|
||||
Spam(removesItem = true),
|
||||
Move(removesItem = true)
|
||||
}
|
||||
121
app/core/src/main/java/com/fsck/k9/TimberLogger.kt
Normal file
121
app/core/src/main/java/com/fsck/k9/TimberLogger.kt
Normal file
|
|
@ -0,0 +1,121 @@
|
|||
package com.fsck.k9
|
||||
|
||||
import android.os.Build
|
||||
import com.fsck.k9.logging.Logger
|
||||
import java.util.regex.Pattern
|
||||
import timber.log.Timber
|
||||
|
||||
class TimberLogger : Logger {
|
||||
override fun v(message: String?, vararg args: Any?) {
|
||||
setTimberTag()
|
||||
Timber.v(message, *args)
|
||||
}
|
||||
|
||||
override fun v(t: Throwable?, message: String?, vararg args: Any?) {
|
||||
setTimberTag()
|
||||
Timber.v(t, message, *args)
|
||||
}
|
||||
|
||||
override fun v(t: Throwable?) {
|
||||
setTimberTag()
|
||||
Timber.v(t)
|
||||
}
|
||||
|
||||
override fun d(message: String?, vararg args: Any?) {
|
||||
setTimberTag()
|
||||
Timber.d(message, *args)
|
||||
}
|
||||
|
||||
override fun d(t: Throwable?, message: String?, vararg args: Any?) {
|
||||
setTimberTag()
|
||||
Timber.d(t, message, *args)
|
||||
}
|
||||
|
||||
override fun d(t: Throwable?) {
|
||||
setTimberTag()
|
||||
Timber.d(t)
|
||||
}
|
||||
|
||||
override fun i(message: String?, vararg args: Any?) {
|
||||
setTimberTag()
|
||||
Timber.i(message, *args)
|
||||
}
|
||||
|
||||
override fun i(t: Throwable?, message: String?, vararg args: Any?) {
|
||||
setTimberTag()
|
||||
Timber.i(t, message, *args)
|
||||
}
|
||||
|
||||
override fun i(t: Throwable?) {
|
||||
setTimberTag()
|
||||
Timber.i(t)
|
||||
}
|
||||
|
||||
override fun w(message: String?, vararg args: Any?) {
|
||||
setTimberTag()
|
||||
Timber.w(message, *args)
|
||||
}
|
||||
|
||||
override fun w(t: Throwable?, message: String?, vararg args: Any?) {
|
||||
setTimberTag()
|
||||
Timber.w(t, message, *args)
|
||||
}
|
||||
|
||||
override fun w(t: Throwable?) {
|
||||
setTimberTag()
|
||||
Timber.w(t)
|
||||
}
|
||||
|
||||
override fun e(message: String?, vararg args: Any?) {
|
||||
setTimberTag()
|
||||
Timber.e(message, *args)
|
||||
}
|
||||
|
||||
override fun e(t: Throwable?, message: String?, vararg args: Any?) {
|
||||
setTimberTag()
|
||||
Timber.e(t, message, *args)
|
||||
}
|
||||
|
||||
override fun e(t: Throwable?) {
|
||||
setTimberTag()
|
||||
Timber.e(t)
|
||||
}
|
||||
|
||||
private fun setTimberTag() {
|
||||
val tag = Throwable().stackTrace
|
||||
.first { it.className !in IGNORE_CLASSES }
|
||||
.let(::createStackElementTag)
|
||||
|
||||
// We explicitly set a tag, otherwise Timber will always derive the tag "TimberLogger".
|
||||
Timber.tag(tag)
|
||||
}
|
||||
|
||||
private fun createStackElementTag(element: StackTraceElement): String {
|
||||
var tag = element.className.substringAfterLast('.')
|
||||
val matcher = ANONYMOUS_CLASS.matcher(tag)
|
||||
if (matcher.find()) {
|
||||
tag = matcher.replaceAll("")
|
||||
}
|
||||
|
||||
// Tag length limit was removed in API 26.
|
||||
return if (tag.length <= MAX_TAG_LENGTH || Build.VERSION.SDK_INT >= 26) {
|
||||
tag
|
||||
} else {
|
||||
tag.substring(0, MAX_TAG_LENGTH)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val MAX_TAG_LENGTH = 23
|
||||
private val ANONYMOUS_CLASS = Pattern.compile("(\\$\\d+)+$")
|
||||
|
||||
private val IGNORE_CLASSES = setOf(
|
||||
Timber::class.java.name,
|
||||
Timber.Forest::class.java.name,
|
||||
Timber.Tree::class.java.name,
|
||||
Timber.DebugTree::class.java.name,
|
||||
TimberLogger::class.java.name,
|
||||
com.fsck.k9.logging.Timber::class.java.name
|
||||
)
|
||||
}
|
||||
}
|
||||
7
app/core/src/main/java/com/fsck/k9/UiDensity.kt
Normal file
7
app/core/src/main/java/com/fsck/k9/UiDensity.kt
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
package com.fsck.k9
|
||||
|
||||
enum class UiDensity {
|
||||
Compact,
|
||||
Default,
|
||||
Relaxed,
|
||||
}
|
||||
|
|
@ -0,0 +1,70 @@
|
|||
package com.fsck.k9.autocrypt
|
||||
|
||||
import com.fsck.k9.message.CryptoStatus
|
||||
|
||||
data class AutocryptDraftStateHeader(
|
||||
val isEncrypt: Boolean,
|
||||
val isSignOnly: Boolean,
|
||||
val isReply: Boolean,
|
||||
val isByChoice: Boolean,
|
||||
val isPgpInline: Boolean,
|
||||
val parameters: Map<String, String> = mapOf()
|
||||
) {
|
||||
|
||||
fun toHeaderValue(): String {
|
||||
val builder = StringBuilder()
|
||||
|
||||
builder.append(AutocryptDraftStateHeader.PARAM_ENCRYPT)
|
||||
builder.append(if (isEncrypt) "=yes; " else "=no; ")
|
||||
|
||||
if (isReply) {
|
||||
builder.append(AutocryptDraftStateHeader.PARAM_IS_REPLY).append("=yes; ")
|
||||
}
|
||||
if (isSignOnly) {
|
||||
builder.append(AutocryptDraftStateHeader.PARAM_SIGN_ONLY).append("=yes; ")
|
||||
}
|
||||
if (isByChoice) {
|
||||
builder.append(AutocryptDraftStateHeader.PARAM_BY_CHOICE).append("=yes; ")
|
||||
}
|
||||
if (isPgpInline) {
|
||||
builder.append(AutocryptDraftStateHeader.PARAM_PGP_INLINE).append("=yes; ")
|
||||
}
|
||||
|
||||
return builder.toString()
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val AUTOCRYPT_DRAFT_STATE_HEADER = "Autocrypt-Draft-State"
|
||||
|
||||
const val PARAM_ENCRYPT = "encrypt"
|
||||
|
||||
const val PARAM_IS_REPLY = "_is-reply-to-encrypted"
|
||||
const val PARAM_BY_CHOICE = "_by-choice"
|
||||
const val PARAM_PGP_INLINE = "_pgp-inline"
|
||||
const val PARAM_SIGN_ONLY = "_sign-only"
|
||||
|
||||
const val VALUE_YES = "yes"
|
||||
|
||||
@JvmStatic
|
||||
fun fromCryptoStatus(cryptoStatus: CryptoStatus): AutocryptDraftStateHeader {
|
||||
if (cryptoStatus.isSignOnly) {
|
||||
return AutocryptDraftStateHeader(
|
||||
false,
|
||||
true,
|
||||
cryptoStatus.isReplyToEncrypted,
|
||||
cryptoStatus.isUserChoice(),
|
||||
cryptoStatus.isPgpInlineModeEnabled,
|
||||
mapOf()
|
||||
)
|
||||
}
|
||||
return AutocryptDraftStateHeader(
|
||||
cryptoStatus.isEncryptionEnabled,
|
||||
false,
|
||||
cryptoStatus.isReplyToEncrypted,
|
||||
cryptoStatus.isUserChoice(),
|
||||
cryptoStatus.isPgpInlineModeEnabled,
|
||||
mapOf()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
package com.fsck.k9.autocrypt
|
||||
|
||||
import com.fsck.k9.mail.internet.MimeUtility
|
||||
|
||||
class AutocryptDraftStateHeaderParser internal constructor() {
|
||||
|
||||
fun parseAutocryptDraftStateHeader(headerValue: String): AutocryptDraftStateHeader? {
|
||||
val parameters = MimeUtility.getAllHeaderParameters(headerValue)
|
||||
|
||||
val isEncryptStr = parameters.remove(AutocryptDraftStateHeader.PARAM_ENCRYPT) ?: return null
|
||||
val isEncrypt = isEncryptStr == AutocryptDraftStateHeader.VALUE_YES
|
||||
|
||||
val isSignOnlyStr = parameters.remove(AutocryptDraftStateHeader.PARAM_SIGN_ONLY)
|
||||
val isSignOnly = isSignOnlyStr == AutocryptDraftStateHeader.VALUE_YES
|
||||
|
||||
val isReplyStr = parameters.remove(AutocryptDraftStateHeader.PARAM_IS_REPLY)
|
||||
val isReply = isReplyStr == AutocryptDraftStateHeader.VALUE_YES
|
||||
|
||||
val isByChoiceStr = parameters.remove(AutocryptDraftStateHeader.PARAM_BY_CHOICE)
|
||||
val isByChoice = isByChoiceStr == AutocryptDraftStateHeader.VALUE_YES
|
||||
|
||||
val isPgpInlineStr = parameters.remove(AutocryptDraftStateHeader.PARAM_PGP_INLINE)
|
||||
val isPgpInline = isPgpInlineStr == AutocryptDraftStateHeader.VALUE_YES
|
||||
|
||||
if (hasCriticalParameters(parameters)) {
|
||||
return null
|
||||
}
|
||||
|
||||
return AutocryptDraftStateHeader(isEncrypt, isSignOnly, isReply, isByChoice, isPgpInline, parameters)
|
||||
}
|
||||
|
||||
private fun hasCriticalParameters(parameters: Map<String, String>): Boolean {
|
||||
for (parameterName in parameters.keys) {
|
||||
if (!parameterName.startsWith("_")) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,60 @@
|
|||
package com.fsck.k9.autocrypt;
|
||||
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
|
||||
class AutocryptGossipHeader {
|
||||
static final String AUTOCRYPT_GOSSIP_HEADER = "Autocrypt-Gossip";
|
||||
|
||||
private static final String AUTOCRYPT_PARAM_ADDR = "addr";
|
||||
private static final String AUTOCRYPT_PARAM_KEY_DATA = "keydata";
|
||||
|
||||
|
||||
@NonNull
|
||||
final byte[] keyData;
|
||||
@NonNull
|
||||
final String addr;
|
||||
|
||||
AutocryptGossipHeader(@NonNull String addr, @NonNull byte[] keyData) {
|
||||
this.addr = addr;
|
||||
this.keyData = keyData;
|
||||
}
|
||||
|
||||
String toRawHeaderString() {
|
||||
StringBuilder builder = new StringBuilder();
|
||||
|
||||
builder.append(AutocryptGossipHeader.AUTOCRYPT_GOSSIP_HEADER).append(": ");
|
||||
builder.append(AutocryptGossipHeader.AUTOCRYPT_PARAM_ADDR).append('=').append(addr).append("; ");
|
||||
builder.append(AutocryptGossipHeader.AUTOCRYPT_PARAM_KEY_DATA).append('=');
|
||||
builder.append(AutocryptHeader.createFoldedBase64KeyData(keyData));
|
||||
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) {
|
||||
return true;
|
||||
}
|
||||
if (o == null || getClass() != o.getClass()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
AutocryptGossipHeader that = (AutocryptGossipHeader) o;
|
||||
|
||||
if (!Arrays.equals(keyData, that.keyData)) {
|
||||
return false;
|
||||
}
|
||||
return addr.equals(that.addr);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int result = Arrays.hashCode(keyData);
|
||||
result = 31 * result + addr.hashCode();
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,95 @@
|
|||
package com.fsck.k9.autocrypt;
|
||||
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
|
||||
import com.fsck.k9.mail.Part;
|
||||
import com.fsck.k9.mail.internet.MimeUtility;
|
||||
import okio.ByteString;
|
||||
import timber.log.Timber;
|
||||
|
||||
|
||||
class AutocryptGossipHeaderParser {
|
||||
private static final AutocryptGossipHeaderParser INSTANCE = new AutocryptGossipHeaderParser();
|
||||
|
||||
|
||||
public static AutocryptGossipHeaderParser getInstance() {
|
||||
return INSTANCE;
|
||||
}
|
||||
|
||||
private AutocryptGossipHeaderParser() { }
|
||||
|
||||
|
||||
List<AutocryptGossipHeader> getAllAutocryptGossipHeaders(Part part) {
|
||||
String[] headers = part.getHeader(AutocryptGossipHeader.AUTOCRYPT_GOSSIP_HEADER);
|
||||
List<AutocryptGossipHeader> autocryptHeaders = parseAllAutocryptGossipHeaders(headers);
|
||||
|
||||
return Collections.unmodifiableList(autocryptHeaders);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@VisibleForTesting
|
||||
AutocryptGossipHeader parseAutocryptGossipHeader(String headerValue) {
|
||||
Map<String,String> parameters = MimeUtility.getAllHeaderParameters(headerValue);
|
||||
|
||||
String type = parameters.remove(AutocryptHeader.AUTOCRYPT_PARAM_TYPE);
|
||||
if (type != null && !type.equals(AutocryptHeader.AUTOCRYPT_TYPE_1)) {
|
||||
Timber.e("autocrypt: unsupported type parameter %s", type);
|
||||
return null;
|
||||
}
|
||||
|
||||
String base64KeyData = parameters.remove(AutocryptHeader.AUTOCRYPT_PARAM_KEY_DATA);
|
||||
if (base64KeyData == null) {
|
||||
Timber.e("autocrypt: missing key parameter");
|
||||
return null;
|
||||
}
|
||||
|
||||
ByteString byteString = ByteString.decodeBase64(base64KeyData);
|
||||
if (byteString == null) {
|
||||
Timber.e("autocrypt: error parsing base64 data");
|
||||
return null;
|
||||
}
|
||||
|
||||
String addr = parameters.remove(AutocryptHeader.AUTOCRYPT_PARAM_ADDR);
|
||||
if (addr == null) {
|
||||
Timber.e("autocrypt: no to header!");
|
||||
return null;
|
||||
}
|
||||
|
||||
if (hasCriticalParameters(parameters)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new AutocryptGossipHeader(addr, byteString.toByteArray());
|
||||
}
|
||||
|
||||
private boolean hasCriticalParameters(Map<String, String> parameters) {
|
||||
for (String parameterName : parameters.keySet()) {
|
||||
if (parameterName != null && !parameterName.startsWith("_")) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private List<AutocryptGossipHeader> parseAllAutocryptGossipHeaders(String[] headers) {
|
||||
ArrayList<AutocryptGossipHeader> autocryptHeaders = new ArrayList<>();
|
||||
for (String header : headers) {
|
||||
AutocryptGossipHeader autocryptHeader = parseAutocryptGossipHeader(header);
|
||||
if (autocryptHeader == null) {
|
||||
Timber.e("Encountered malformed autocrypt-gossip header - skipping!");
|
||||
continue;
|
||||
}
|
||||
autocryptHeaders.add(autocryptHeader);
|
||||
}
|
||||
return autocryptHeaders;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,102 @@
|
|||
package com.fsck.k9.autocrypt;
|
||||
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Map;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import okio.ByteString;
|
||||
|
||||
|
||||
class AutocryptHeader {
|
||||
static final String AUTOCRYPT_HEADER = "Autocrypt";
|
||||
|
||||
static final String AUTOCRYPT_PARAM_ADDR = "addr";
|
||||
static final String AUTOCRYPT_PARAM_KEY_DATA = "keydata";
|
||||
|
||||
static final String AUTOCRYPT_PARAM_TYPE = "type";
|
||||
static final String AUTOCRYPT_TYPE_1 = "1";
|
||||
|
||||
static final String AUTOCRYPT_PARAM_PREFER_ENCRYPT = "prefer-encrypt";
|
||||
static final String AUTOCRYPT_PREFER_ENCRYPT_MUTUAL = "mutual";
|
||||
|
||||
private static final int HEADER_LINE_LENGTH = 76;
|
||||
|
||||
|
||||
@NonNull
|
||||
final byte[] keyData;
|
||||
@NonNull
|
||||
final String addr;
|
||||
@NonNull
|
||||
final Map<String,String> parameters;
|
||||
final boolean isPreferEncryptMutual;
|
||||
|
||||
AutocryptHeader(@NonNull Map<String, String> parameters, @NonNull String addr,
|
||||
@NonNull byte[] keyData, boolean isPreferEncryptMutual) {
|
||||
this.parameters = parameters;
|
||||
this.addr = addr;
|
||||
this.keyData = keyData;
|
||||
this.isPreferEncryptMutual = isPreferEncryptMutual;
|
||||
}
|
||||
|
||||
String toRawHeaderString() {
|
||||
// TODO we don't properly fold lines here. if we want to support parameters, we need to do that somehow
|
||||
if (!parameters.isEmpty()) {
|
||||
throw new UnsupportedOperationException("arbitrary parameters not supported");
|
||||
}
|
||||
|
||||
StringBuilder builder = new StringBuilder();
|
||||
builder.append(AutocryptHeader.AUTOCRYPT_HEADER).append(": ");
|
||||
builder.append(AutocryptHeader.AUTOCRYPT_PARAM_ADDR).append('=').append(addr).append("; ");
|
||||
if (isPreferEncryptMutual) {
|
||||
builder.append(AutocryptHeader.AUTOCRYPT_PARAM_PREFER_ENCRYPT)
|
||||
.append('=').append(AutocryptHeader.AUTOCRYPT_PREFER_ENCRYPT_MUTUAL).append("; ");
|
||||
}
|
||||
builder.append(AutocryptHeader.AUTOCRYPT_PARAM_KEY_DATA).append("=");
|
||||
builder.append(createFoldedBase64KeyData(keyData));
|
||||
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
static String createFoldedBase64KeyData(byte[] keyData) {
|
||||
String base64KeyData = ByteString.of(keyData).base64();
|
||||
StringBuilder result = new StringBuilder();
|
||||
|
||||
for (int i = 0, base64Length = base64KeyData.length(); i < base64Length; i += HEADER_LINE_LENGTH) {
|
||||
if (i + HEADER_LINE_LENGTH <= base64Length) {
|
||||
result.append("\r\n ");
|
||||
result.append(base64KeyData, i, i + HEADER_LINE_LENGTH);
|
||||
} else {
|
||||
result.append("\r\n ");
|
||||
result.append(base64KeyData, i, base64Length);
|
||||
}
|
||||
}
|
||||
|
||||
return result.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) {
|
||||
return true;
|
||||
}
|
||||
if (o == null || getClass() != o.getClass()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
AutocryptHeader that = (AutocryptHeader) o;
|
||||
|
||||
return isPreferEncryptMutual == that.isPreferEncryptMutual && Arrays.equals(keyData, that.keyData)
|
||||
&& addr.equals(that.addr) && parameters.equals(that.parameters);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int result = Arrays.hashCode(keyData);
|
||||
result = 31 * result + addr.hashCode();
|
||||
result = 31 * result + parameters.hashCode();
|
||||
result = 31 * result + (isPreferEncryptMutual ? 1 : 0);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,99 @@
|
|||
package com.fsck.k9.autocrypt;
|
||||
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Map;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
|
||||
import com.fsck.k9.mail.Message;
|
||||
import com.fsck.k9.mail.internet.MimeUtility;
|
||||
import okio.ByteString;
|
||||
import timber.log.Timber;
|
||||
|
||||
|
||||
class AutocryptHeaderParser {
|
||||
private static final AutocryptHeaderParser INSTANCE = new AutocryptHeaderParser();
|
||||
|
||||
|
||||
public static AutocryptHeaderParser getInstance() {
|
||||
return INSTANCE;
|
||||
}
|
||||
|
||||
private AutocryptHeaderParser() { }
|
||||
|
||||
|
||||
@Nullable
|
||||
AutocryptHeader getValidAutocryptHeader(Message currentMessage) {
|
||||
String[] headers = currentMessage.getHeader(AutocryptHeader.AUTOCRYPT_HEADER);
|
||||
ArrayList<AutocryptHeader> autocryptHeaders = parseAllAutocryptHeaders(headers);
|
||||
|
||||
boolean isSingleValidHeader = autocryptHeaders.size() == 1;
|
||||
return isSingleValidHeader ? autocryptHeaders.get(0) : null;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@VisibleForTesting
|
||||
AutocryptHeader parseAutocryptHeader(String headerValue) {
|
||||
Map<String,String> parameters = MimeUtility.getAllHeaderParameters(headerValue);
|
||||
|
||||
String type = parameters.remove(AutocryptHeader.AUTOCRYPT_PARAM_TYPE);
|
||||
if (type != null && !type.equals(AutocryptHeader.AUTOCRYPT_TYPE_1)) {
|
||||
Timber.e("autocrypt: unsupported type parameter %s", type);
|
||||
return null;
|
||||
}
|
||||
|
||||
String base64KeyData = parameters.remove(AutocryptHeader.AUTOCRYPT_PARAM_KEY_DATA);
|
||||
if (base64KeyData == null) {
|
||||
Timber.e("autocrypt: missing key parameter");
|
||||
return null;
|
||||
}
|
||||
|
||||
ByteString byteString = ByteString.decodeBase64(base64KeyData);
|
||||
if (byteString == null) {
|
||||
Timber.e("autocrypt: error parsing base64 data");
|
||||
return null;
|
||||
}
|
||||
|
||||
String to = parameters.remove(AutocryptHeader.AUTOCRYPT_PARAM_ADDR);
|
||||
if (to == null) {
|
||||
Timber.e("autocrypt: no to header!");
|
||||
return null;
|
||||
}
|
||||
|
||||
boolean isPreferEncryptMutual = false;
|
||||
String preferEncrypt = parameters.remove(AutocryptHeader.AUTOCRYPT_PARAM_PREFER_ENCRYPT);
|
||||
if (AutocryptHeader.AUTOCRYPT_PREFER_ENCRYPT_MUTUAL.equalsIgnoreCase(preferEncrypt)) {
|
||||
isPreferEncryptMutual = true;
|
||||
}
|
||||
|
||||
if (hasCriticalParameters(parameters)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new AutocryptHeader(parameters, to, byteString.toByteArray(), isPreferEncryptMutual);
|
||||
}
|
||||
|
||||
private boolean hasCriticalParameters(Map<String, String> parameters) {
|
||||
for (String parameterName : parameters.keySet()) {
|
||||
if (parameterName != null && !parameterName.startsWith("_")) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private ArrayList<AutocryptHeader> parseAllAutocryptHeaders(String[] headers) {
|
||||
ArrayList<AutocryptHeader> autocryptHeaders = new ArrayList<>();
|
||||
for (String header : headers) {
|
||||
AutocryptHeader autocryptHeader = parseAutocryptHeader(header);
|
||||
if (autocryptHeader != null) {
|
||||
autocryptHeaders.add(autocryptHeader);
|
||||
}
|
||||
}
|
||||
return autocryptHeaders;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
package com.fsck.k9.autocrypt;
|
||||
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.InputStream;
|
||||
|
||||
import android.content.Intent;
|
||||
|
||||
import org.openintents.openpgp.util.OpenPgpApi;
|
||||
|
||||
|
||||
public class AutocryptOpenPgpApiInteractor {
|
||||
public static AutocryptOpenPgpApiInteractor getInstance() {
|
||||
return new AutocryptOpenPgpApiInteractor();
|
||||
}
|
||||
|
||||
private AutocryptOpenPgpApiInteractor() { }
|
||||
|
||||
public byte[] getKeyMaterialForKeyId(OpenPgpApi openPgpApi, long keyId, String minimizeForUserId) {
|
||||
Intent retrieveKeyIntent = new Intent(OpenPgpApi.ACTION_GET_KEY);
|
||||
retrieveKeyIntent.putExtra(OpenPgpApi.EXTRA_KEY_ID, keyId);
|
||||
return getKeyMaterialFromApi(openPgpApi, retrieveKeyIntent, minimizeForUserId);
|
||||
}
|
||||
|
||||
public byte[] getKeyMaterialForUserId(OpenPgpApi openPgpApi, String userId) {
|
||||
Intent retrieveKeyIntent = new Intent(OpenPgpApi.ACTION_GET_KEY);
|
||||
retrieveKeyIntent.putExtra(OpenPgpApi.EXTRA_USER_ID, userId);
|
||||
return getKeyMaterialFromApi(openPgpApi, retrieveKeyIntent, userId);
|
||||
}
|
||||
|
||||
private byte[] getKeyMaterialFromApi(OpenPgpApi openPgpApi, Intent retrieveKeyIntent, String userId) {
|
||||
retrieveKeyIntent.putExtra(OpenPgpApi.EXTRA_MINIMIZE, true);
|
||||
retrieveKeyIntent.putExtra(OpenPgpApi.EXTRA_MINIMIZE_USER_ID, userId);
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
Intent result = openPgpApi.executeApi(retrieveKeyIntent, (InputStream) null, baos);
|
||||
|
||||
if (result.getIntExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_ERROR) ==
|
||||
OpenPgpApi.RESULT_CODE_SUCCESS) {
|
||||
return baos.toByteArray();
|
||||
} else{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,164 @@
|
|||
package com.fsck.k9.autocrypt;
|
||||
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.fsck.k9.mail.Address;
|
||||
import com.fsck.k9.mail.Message;
|
||||
import com.fsck.k9.mail.Message.RecipientType;
|
||||
import com.fsck.k9.mail.internet.MimeBodyPart;
|
||||
import org.openintents.openpgp.AutocryptPeerUpdate;
|
||||
import org.openintents.openpgp.util.OpenPgpApi;
|
||||
|
||||
|
||||
public class AutocryptOperations {
|
||||
private final AutocryptHeaderParser autocryptHeaderParser;
|
||||
private final AutocryptGossipHeaderParser autocryptGossipHeaderParser;
|
||||
|
||||
|
||||
public static AutocryptOperations getInstance() {
|
||||
AutocryptHeaderParser autocryptHeaderParser = AutocryptHeaderParser.getInstance();
|
||||
AutocryptGossipHeaderParser autocryptGossipHeaderParser = AutocryptGossipHeaderParser.getInstance();
|
||||
return new AutocryptOperations(autocryptHeaderParser, autocryptGossipHeaderParser);
|
||||
}
|
||||
|
||||
|
||||
private AutocryptOperations(AutocryptHeaderParser autocryptHeaderParser,
|
||||
AutocryptGossipHeaderParser autocryptGossipHeaderParser) {
|
||||
this.autocryptHeaderParser = autocryptHeaderParser;
|
||||
this.autocryptGossipHeaderParser = autocryptGossipHeaderParser;
|
||||
}
|
||||
|
||||
public boolean addAutocryptPeerUpdateToIntentIfPresent(Message currentMessage, Intent intent) {
|
||||
AutocryptHeader autocryptHeader = autocryptHeaderParser.getValidAutocryptHeader(currentMessage);
|
||||
if (autocryptHeader == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
String messageFromAddress = currentMessage.getFrom()[0].getAddress();
|
||||
if (!autocryptHeader.addr.equalsIgnoreCase(messageFromAddress)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Date messageDate = currentMessage.getSentDate();
|
||||
Date internalDate = currentMessage.getInternalDate();
|
||||
Date effectiveDate = messageDate.before(internalDate) ? messageDate : internalDate;
|
||||
|
||||
AutocryptPeerUpdate data = AutocryptPeerUpdate.create(
|
||||
autocryptHeader.keyData, effectiveDate, autocryptHeader.isPreferEncryptMutual);
|
||||
intent.putExtra(OpenPgpApi.EXTRA_AUTOCRYPT_PEER_ID, messageFromAddress);
|
||||
intent.putExtra(OpenPgpApi.EXTRA_AUTOCRYPT_PEER_UPDATE, data);
|
||||
return true;
|
||||
}
|
||||
|
||||
public boolean addAutocryptGossipUpdateToIntentIfPresent(Message message, MimeBodyPart decryptedPart, Intent intent) {
|
||||
Bundle updates = createGossipUpdateBundle(message, decryptedPart);
|
||||
|
||||
if (updates == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
intent.putExtra(OpenPgpApi.EXTRA_AUTOCRYPT_PEER_GOSSIP_UPDATES, updates);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private Bundle createGossipUpdateBundle(Message message, MimeBodyPart decryptedPart) {
|
||||
List<String> gossipAcceptedAddresses = getGossipAcceptedAddresses(message);
|
||||
if (gossipAcceptedAddresses.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
List<AutocryptGossipHeader> autocryptGossipHeaders =
|
||||
autocryptGossipHeaderParser.getAllAutocryptGossipHeaders(decryptedPart);
|
||||
if (autocryptGossipHeaders.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Date messageDate = message.getSentDate();
|
||||
Date internalDate = message.getInternalDate();
|
||||
Date effectiveDate = messageDate.before(internalDate) ? messageDate : internalDate;
|
||||
|
||||
return createGossipUpdateBundle(gossipAcceptedAddresses, autocryptGossipHeaders, effectiveDate);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private Bundle createGossipUpdateBundle(List<String> gossipAcceptedAddresses,
|
||||
List<AutocryptGossipHeader> autocryptGossipHeaders, Date effectiveDate) {
|
||||
Bundle updates = new Bundle();
|
||||
for (AutocryptGossipHeader autocryptGossipHeader : autocryptGossipHeaders) {
|
||||
String normalizedAddress = autocryptGossipHeader.addr.toLowerCase(Locale.ROOT);
|
||||
boolean isAcceptedAddress = gossipAcceptedAddresses.contains(normalizedAddress);
|
||||
if (!isAcceptedAddress) {
|
||||
continue;
|
||||
}
|
||||
|
||||
AutocryptPeerUpdate update = AutocryptPeerUpdate.create(autocryptGossipHeader.keyData, effectiveDate, false);
|
||||
updates.putParcelable(autocryptGossipHeader.addr, update);
|
||||
}
|
||||
if (updates.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
return updates;
|
||||
}
|
||||
|
||||
private List<String> getGossipAcceptedAddresses(Message message) {
|
||||
ArrayList<String> result = new ArrayList<>();
|
||||
|
||||
addRecipientsToList(result, message, RecipientType.TO);
|
||||
addRecipientsToList(result, message, RecipientType.CC);
|
||||
removeRecipientsFromList(result, message, RecipientType.DELIVERED_TO);
|
||||
|
||||
return Collections.unmodifiableList(result);
|
||||
}
|
||||
|
||||
private void addRecipientsToList(ArrayList<String> result, Message message, RecipientType recipientType) {
|
||||
for (Address address : message.getRecipients(recipientType)) {
|
||||
String addr = address.getAddress();
|
||||
if (addr != null) {
|
||||
result.add(addr.toLowerCase(Locale.ROOT));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void removeRecipientsFromList(ArrayList<String> result, Message message, RecipientType recipientType) {
|
||||
for (Address address : message.getRecipients(recipientType)) {
|
||||
String addr = address.getAddress();
|
||||
if (addr != null) {
|
||||
result.remove(addr);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public boolean hasAutocryptHeader(Message currentMessage) {
|
||||
return currentMessage.getHeader(AutocryptHeader.AUTOCRYPT_HEADER).length > 0;
|
||||
}
|
||||
|
||||
public boolean hasAutocryptGossipHeader(MimeBodyPart part) {
|
||||
return part.getHeader(AutocryptGossipHeader.AUTOCRYPT_GOSSIP_HEADER).length > 0;
|
||||
}
|
||||
|
||||
public void addAutocryptHeaderToMessage(Message message, byte[] keyData,
|
||||
String autocryptAddress, boolean preferEncryptMutual) {
|
||||
AutocryptHeader autocryptHeader = new AutocryptHeader(
|
||||
Collections.<String,String>emptyMap(), autocryptAddress, keyData, preferEncryptMutual);
|
||||
String rawAutocryptHeader = autocryptHeader.toRawHeaderString();
|
||||
|
||||
message.addRawHeader(AutocryptHeader.AUTOCRYPT_HEADER, rawAutocryptHeader);
|
||||
}
|
||||
|
||||
public void addAutocryptGossipHeaderToPart(MimeBodyPart part, byte[] keyData, String autocryptAddress) {
|
||||
AutocryptGossipHeader autocryptGossipHeader = new AutocryptGossipHeader(autocryptAddress, keyData);
|
||||
String rawAutocryptHeader = autocryptGossipHeader.toRawHeaderString();
|
||||
|
||||
part.addRawHeader(AutocryptGossipHeader.AUTOCRYPT_GOSSIP_HEADER, rawAutocryptHeader);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
package com.fsck.k9.autocrypt
|
||||
|
||||
interface AutocryptStringProvider {
|
||||
fun transferMessageSubject(): String
|
||||
fun transferMessageBody(): String
|
||||
}
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
package com.fsck.k9.autocrypt
|
||||
|
||||
import com.fsck.k9.K9
|
||||
import com.fsck.k9.mail.Address
|
||||
import com.fsck.k9.mail.Flag
|
||||
import com.fsck.k9.mail.Message
|
||||
import com.fsck.k9.mail.MessagingException
|
||||
import com.fsck.k9.mail.internet.MimeBodyPart
|
||||
import com.fsck.k9.mail.internet.MimeHeader
|
||||
import com.fsck.k9.mail.internet.MimeMessage
|
||||
import com.fsck.k9.mail.internet.MimeMessageHelper
|
||||
import com.fsck.k9.mail.internet.MimeMultipart
|
||||
import com.fsck.k9.mail.internet.TextBody
|
||||
import com.fsck.k9.mailstore.BinaryMemoryBody
|
||||
import java.util.Date
|
||||
|
||||
class AutocryptTransferMessageCreator(private val stringProvider: AutocryptStringProvider) {
|
||||
fun createAutocryptTransferMessage(data: ByteArray, address: Address): Message {
|
||||
try {
|
||||
val subjectText = stringProvider.transferMessageSubject()
|
||||
val messageText = stringProvider.transferMessageBody()
|
||||
|
||||
val textBodyPart = MimeBodyPart.create(TextBody(messageText))
|
||||
val dataBodyPart = MimeBodyPart.create(BinaryMemoryBody(data, "7bit"))
|
||||
dataBodyPart.setHeader(MimeHeader.HEADER_CONTENT_TYPE, "application/autocrypt-setup")
|
||||
dataBodyPart.setHeader(MimeHeader.HEADER_CONTENT_DISPOSITION, "attachment; filename=\"autocrypt-setup-message\"")
|
||||
|
||||
val messageBody = MimeMultipart.newInstance()
|
||||
messageBody.addBodyPart(textBodyPart)
|
||||
messageBody.addBodyPart(dataBodyPart)
|
||||
|
||||
val message = MimeMessage.create()
|
||||
MimeMessageHelper.setBody(message, messageBody)
|
||||
|
||||
val nowDate = Date()
|
||||
|
||||
message.setFlag(Flag.X_DOWNLOADED_FULL, true)
|
||||
message.subject = subjectText
|
||||
message.setHeader("Autocrypt-Setup-Message", "v1")
|
||||
message.internalDate = nowDate
|
||||
message.addSentDate(nowDate, K9.isHideTimeZone)
|
||||
message.setFrom(address)
|
||||
message.setHeader("To", address.toEncodedString())
|
||||
|
||||
return message
|
||||
} catch (e: MessagingException) {
|
||||
throw AssertionError(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
package com.fsck.k9.autocrypt
|
||||
|
||||
import org.koin.dsl.module
|
||||
|
||||
val autocryptModule = module {
|
||||
single { AutocryptTransferMessageCreator(get()) }
|
||||
single { AutocryptDraftStateHeaderParser() }
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
package com.fsck.k9.backend
|
||||
|
||||
import com.fsck.k9.Account
|
||||
import com.fsck.k9.backend.api.Backend
|
||||
|
||||
interface BackendFactory {
|
||||
fun createBackend(account: Account): Backend
|
||||
}
|
||||
75
app/core/src/main/java/com/fsck/k9/backend/BackendManager.kt
Normal file
75
app/core/src/main/java/com/fsck/k9/backend/BackendManager.kt
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
package com.fsck.k9.backend
|
||||
|
||||
import com.fsck.k9.Account
|
||||
import com.fsck.k9.backend.api.Backend
|
||||
import com.fsck.k9.mail.ServerSettings
|
||||
import java.util.concurrent.CopyOnWriteArraySet
|
||||
|
||||
class BackendManager(private val backendFactories: Map<String, BackendFactory>) {
|
||||
private val backendCache = mutableMapOf<String, BackendContainer>()
|
||||
private val listeners = CopyOnWriteArraySet<BackendChangedListener>()
|
||||
|
||||
fun getBackend(account: Account): Backend {
|
||||
val newBackend = synchronized(backendCache) {
|
||||
val container = backendCache[account.uuid]
|
||||
if (container != null && isBackendStillValid(container, account)) {
|
||||
return container.backend
|
||||
}
|
||||
|
||||
createBackend(account).also { backend ->
|
||||
backendCache[account.uuid] = BackendContainer(
|
||||
backend,
|
||||
account.incomingServerSettings,
|
||||
account.outgoingServerSettings
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
notifyListeners(account)
|
||||
|
||||
return newBackend
|
||||
}
|
||||
|
||||
private fun isBackendStillValid(container: BackendContainer, account: Account): Boolean {
|
||||
return container.incomingServerSettings == account.incomingServerSettings &&
|
||||
container.outgoingServerSettings == account.outgoingServerSettings
|
||||
}
|
||||
|
||||
fun removeBackend(account: Account) {
|
||||
synchronized(backendCache) {
|
||||
backendCache.remove(account.uuid)
|
||||
}
|
||||
|
||||
notifyListeners(account)
|
||||
}
|
||||
|
||||
private fun createBackend(account: Account): Backend {
|
||||
val serverType = account.incomingServerSettings.type
|
||||
val backendFactory = backendFactories[serverType] ?: error("Unsupported account type")
|
||||
return backendFactory.createBackend(account)
|
||||
}
|
||||
|
||||
fun addListener(listener: BackendChangedListener) {
|
||||
listeners.add(listener)
|
||||
}
|
||||
|
||||
fun removeListener(listener: BackendChangedListener) {
|
||||
listeners.remove(listener)
|
||||
}
|
||||
|
||||
private fun notifyListeners(account: Account) {
|
||||
for (listener in listeners) {
|
||||
listener.onBackendChanged(account)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private data class BackendContainer(
|
||||
val backend: Backend,
|
||||
val incomingServerSettings: ServerSettings,
|
||||
val outgoingServerSettings: ServerSettings
|
||||
)
|
||||
|
||||
fun interface BackendChangedListener {
|
||||
fun onBackendChanged(account: Account)
|
||||
}
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
package com.fsck.k9.contact
|
||||
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.provider.ContactsContract
|
||||
import com.fsck.k9.mail.Address
|
||||
|
||||
object ContactIntentHelper {
|
||||
@JvmStatic
|
||||
fun getContactPickerIntent(): Intent {
|
||||
return Intent(Intent.ACTION_PICK, ContactsContract.CommonDataKinds.Email.CONTENT_URI)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Intent to add information to an existing contact or add a new one.
|
||||
*
|
||||
* @param address An {@link Address} instance containing the email address
|
||||
* of the entity you want to add to the contacts. Optionally
|
||||
* the instance also contains the (display) name of that
|
||||
* entity.
|
||||
*/
|
||||
fun getAddEmailContactIntent(address: Address): Intent {
|
||||
return Intent(ContactsContract.Intents.SHOW_OR_CREATE_CONTACT).apply {
|
||||
flags = Intent.FLAG_ACTIVITY_NEW_TASK
|
||||
data = Uri.fromParts("mailto", address.address, null)
|
||||
putExtra(ContactsContract.Intents.EXTRA_CREATE_DESCRIPTION, address.toString())
|
||||
|
||||
if (address.personal != null) {
|
||||
putExtra(ContactsContract.Intents.Insert.NAME, address.personal)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Intent to add a phone number to an existing contact or add a new one.
|
||||
*
|
||||
* @param phoneNumber
|
||||
* The phone number to add to a contact, or to use when creating a new contact.
|
||||
*/
|
||||
fun getAddPhoneContactIntent(phoneNumber: String): Intent {
|
||||
return Intent(Intent.ACTION_INSERT_OR_EDIT).apply {
|
||||
flags = Intent.FLAG_ACTIVITY_NEW_TASK
|
||||
type = ContactsContract.Contacts.CONTENT_ITEM_TYPE
|
||||
putExtra(ContactsContract.Intents.Insert.PHONE, Uri.decode(phoneNumber))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
package com.fsck.k9.controller
|
||||
|
||||
import com.fsck.k9.backend.BackendManager
|
||||
|
||||
interface ControllerExtension {
|
||||
fun init(controller: MessagingController, backendManager: BackendManager, controllerInternals: ControllerInternals)
|
||||
|
||||
interface ControllerInternals {
|
||||
fun put(description: String, listener: MessagingListener?, runnable: Runnable)
|
||||
fun putBackground(description: String, listener: MessagingListener?, runnable: Runnable)
|
||||
}
|
||||
}
|
||||
177
app/core/src/main/java/com/fsck/k9/controller/DraftOperations.kt
Normal file
177
app/core/src/main/java/com/fsck/k9/controller/DraftOperations.kt
Normal file
|
|
@ -0,0 +1,177 @@
|
|||
package com.fsck.k9.controller
|
||||
|
||||
import com.fsck.k9.Account
|
||||
import com.fsck.k9.Account.Expunge
|
||||
import com.fsck.k9.K9
|
||||
import com.fsck.k9.backend.api.Backend
|
||||
import com.fsck.k9.controller.MessagingControllerCommands.PendingAppend
|
||||
import com.fsck.k9.controller.MessagingControllerCommands.PendingReplace
|
||||
import com.fsck.k9.mail.FetchProfile
|
||||
import com.fsck.k9.mail.Message
|
||||
import com.fsck.k9.mail.MessageDownloadState
|
||||
import com.fsck.k9.mail.MessagingException
|
||||
import com.fsck.k9.mailstore.LocalFolder
|
||||
import com.fsck.k9.mailstore.LocalMessage
|
||||
import com.fsck.k9.mailstore.MessageStoreManager
|
||||
import com.fsck.k9.mailstore.SaveMessageData
|
||||
import com.fsck.k9.mailstore.SaveMessageDataCreator
|
||||
import org.jetbrains.annotations.NotNull
|
||||
import timber.log.Timber
|
||||
|
||||
internal class DraftOperations(
|
||||
private val messagingController: @NotNull MessagingController,
|
||||
private val messageStoreManager: @NotNull MessageStoreManager,
|
||||
private val saveMessageDataCreator: SaveMessageDataCreator
|
||||
) {
|
||||
|
||||
fun saveDraft(
|
||||
account: Account,
|
||||
message: Message,
|
||||
existingDraftId: Long?,
|
||||
plaintextSubject: String?
|
||||
): Long? {
|
||||
return try {
|
||||
val draftsFolderId = account.draftsFolderId ?: error("No Drafts folder configured")
|
||||
|
||||
val messageId = if (messagingController.supportsUpload(account)) {
|
||||
saveAndUploadDraft(account, message, draftsFolderId, existingDraftId, plaintextSubject)
|
||||
} else {
|
||||
saveDraftLocally(account, message, draftsFolderId, existingDraftId, plaintextSubject)
|
||||
}
|
||||
|
||||
messageId
|
||||
} catch (e: MessagingException) {
|
||||
Timber.e(e, "Unable to save message as draft.")
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private fun saveAndUploadDraft(
|
||||
account: Account,
|
||||
message: Message,
|
||||
folderId: Long,
|
||||
existingDraftId: Long?,
|
||||
subject: String?
|
||||
): Long {
|
||||
val messageStore = messageStoreManager.getMessageStore(account)
|
||||
|
||||
val messageId = messageStore.saveLocalMessage(folderId, message.toSaveMessageData(subject))
|
||||
|
||||
val previousDraftMessage = if (existingDraftId != null) {
|
||||
val localStore = messagingController.getLocalStoreOrThrow(account)
|
||||
val localFolder = localStore.getFolder(folderId)
|
||||
localFolder.open()
|
||||
|
||||
localFolder.getMessage(existingDraftId)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
if (previousDraftMessage != null) {
|
||||
previousDraftMessage.delete()
|
||||
|
||||
val deleteMessageId = previousDraftMessage.databaseId
|
||||
val command = PendingReplace.create(folderId, messageId, deleteMessageId)
|
||||
messagingController.queuePendingCommand(account, command)
|
||||
} else {
|
||||
val fakeMessageServerId = messageStore.getMessageServerId(messageId)
|
||||
if (fakeMessageServerId != null) {
|
||||
val command = PendingAppend.create(folderId, fakeMessageServerId)
|
||||
messagingController.queuePendingCommand(account, command)
|
||||
}
|
||||
}
|
||||
|
||||
messagingController.processPendingCommands(account)
|
||||
|
||||
return messageId
|
||||
}
|
||||
|
||||
private fun saveDraftLocally(
|
||||
account: Account,
|
||||
message: Message,
|
||||
folderId: Long,
|
||||
existingDraftId: Long?,
|
||||
plaintextSubject: String?
|
||||
): Long {
|
||||
val messageStore = messageStoreManager.getMessageStore(account)
|
||||
val messageData = message.toSaveMessageData(plaintextSubject)
|
||||
|
||||
return messageStore.saveLocalMessage(folderId, messageData, existingDraftId)
|
||||
}
|
||||
|
||||
fun processPendingReplace(command: PendingReplace, account: Account) {
|
||||
val localStore = messagingController.getLocalStoreOrThrow(account)
|
||||
val localFolder = localStore.getFolder(command.folderId)
|
||||
localFolder.open()
|
||||
|
||||
val backend = messagingController.getBackend(account)
|
||||
|
||||
val uploadMessageId = command.uploadMessageId
|
||||
val localMessage = localFolder.getMessage(uploadMessageId)
|
||||
if (localMessage == null) {
|
||||
Timber.w("Couldn't find local copy of message to upload [ID: %d]", uploadMessageId)
|
||||
return
|
||||
} else if (!localMessage.uid.startsWith(K9.LOCAL_UID_PREFIX)) {
|
||||
Timber.i("Message [ID: %d] to be uploaded already has a server ID set. Skipping upload.", uploadMessageId)
|
||||
} else {
|
||||
uploadMessage(backend, account, localFolder, localMessage)
|
||||
}
|
||||
|
||||
deleteMessage(backend, account, localFolder, command.deleteMessageId)
|
||||
}
|
||||
|
||||
private fun uploadMessage(
|
||||
backend: Backend,
|
||||
account: Account,
|
||||
localFolder: LocalFolder,
|
||||
localMessage: LocalMessage
|
||||
) {
|
||||
val folderServerId = localFolder.serverId
|
||||
Timber.d("Uploading message [ID: %d] to remote folder '%s'", localMessage.databaseId, folderServerId)
|
||||
|
||||
val fetchProfile = FetchProfile().apply {
|
||||
add(FetchProfile.Item.BODY)
|
||||
}
|
||||
localFolder.fetch(listOf(localMessage), fetchProfile, null)
|
||||
|
||||
val messageServerId = backend.uploadMessage(folderServerId, localMessage)
|
||||
|
||||
if (messageServerId == null) {
|
||||
Timber.w(
|
||||
"Failed to get a server ID for the uploaded message. Removing local copy [ID: %d]",
|
||||
localMessage.databaseId
|
||||
)
|
||||
localMessage.destroy()
|
||||
} else {
|
||||
val oldUid = localMessage.uid
|
||||
|
||||
localMessage.uid = messageServerId
|
||||
localFolder.changeUid(localMessage)
|
||||
|
||||
for (listener in messagingController.listeners) {
|
||||
listener.messageUidChanged(account, localFolder.databaseId, oldUid, localMessage.uid)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun deleteMessage(backend: Backend, account: Account, localFolder: LocalFolder, messageId: Long) {
|
||||
val messageServerId = localFolder.getMessageUidById(messageId) ?: run {
|
||||
Timber.i("Couldn't find local copy of message [ID: %d] to be deleted. Skipping delete.", messageId)
|
||||
return
|
||||
}
|
||||
|
||||
val messageServerIds = listOf(messageServerId)
|
||||
val folderServerId = localFolder.serverId
|
||||
backend.deleteMessages(folderServerId, messageServerIds)
|
||||
|
||||
if (backend.supportsExpunge && account.expungePolicy == Expunge.EXPUNGE_IMMEDIATELY) {
|
||||
backend.expungeMessages(folderServerId, messageServerIds)
|
||||
}
|
||||
|
||||
messagingController.destroyPlaceholderMessages(localFolder, messageServerIds)
|
||||
}
|
||||
|
||||
private fun Message.toSaveMessageData(subject: String?): SaveMessageData {
|
||||
return saveMessageDataCreator.createSaveMessageData(this, MessageDownloadState.FULL, subject)
|
||||
}
|
||||
}
|
||||
36
app/core/src/main/java/com/fsck/k9/controller/KoinModule.kt
Normal file
36
app/core/src/main/java/com/fsck/k9/controller/KoinModule.kt
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
package com.fsck.k9.controller
|
||||
|
||||
import android.content.Context
|
||||
import com.fsck.k9.Preferences
|
||||
import com.fsck.k9.backend.BackendManager
|
||||
import com.fsck.k9.mailstore.LocalStoreProvider
|
||||
import com.fsck.k9.mailstore.MessageStoreManager
|
||||
import com.fsck.k9.mailstore.SaveMessageDataCreator
|
||||
import com.fsck.k9.mailstore.SpecialLocalFoldersCreator
|
||||
import com.fsck.k9.notification.NotificationController
|
||||
import com.fsck.k9.notification.NotificationStrategy
|
||||
import org.koin.core.qualifier.named
|
||||
import org.koin.dsl.module
|
||||
|
||||
val controllerModule = module {
|
||||
single {
|
||||
MessagingController(
|
||||
get<Context>(),
|
||||
get<NotificationController>(),
|
||||
get<NotificationStrategy>(),
|
||||
get<LocalStoreProvider>(),
|
||||
get<BackendManager>(),
|
||||
get<Preferences>(),
|
||||
get<MessageStoreManager>(),
|
||||
get<SaveMessageDataCreator>(),
|
||||
get<SpecialLocalFoldersCreator>(),
|
||||
get(named("controllerExtensions"))
|
||||
)
|
||||
}
|
||||
single<MessageCountsProvider> {
|
||||
DefaultMessageCountsProvider(
|
||||
preferences = get(),
|
||||
messageStoreManager = get()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,124 @@
|
|||
package com.fsck.k9.controller;
|
||||
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Iterator;
|
||||
import java.util.Map;
|
||||
import java.util.Map.Entry;
|
||||
|
||||
import com.fsck.k9.Account;
|
||||
|
||||
|
||||
class MemorizingMessagingListener extends SimpleMessagingListener {
|
||||
Map<String, Memory> memories = new HashMap<>(31);
|
||||
|
||||
synchronized void removeAccount(Account account) {
|
||||
Iterator<Entry<String, Memory>> memIt = memories.entrySet().iterator();
|
||||
|
||||
while (memIt.hasNext()) {
|
||||
Entry<String, Memory> memoryEntry = memIt.next();
|
||||
|
||||
String uuidForMemory = memoryEntry.getValue().account.getUuid();
|
||||
|
||||
if (uuidForMemory.equals(account.getUuid())) {
|
||||
memIt.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
synchronized void refreshOther(MessagingListener other) {
|
||||
if (other != null) {
|
||||
|
||||
Memory syncStarted = null;
|
||||
|
||||
for (Memory memory : memories.values()) {
|
||||
|
||||
if (memory.syncingState != null) {
|
||||
switch (memory.syncingState) {
|
||||
case STARTED:
|
||||
syncStarted = memory;
|
||||
break;
|
||||
case FINISHED:
|
||||
other.synchronizeMailboxFinished(memory.account, memory.folderId);
|
||||
break;
|
||||
case FAILED:
|
||||
other.synchronizeMailboxFailed(memory.account, memory.folderId,
|
||||
memory.failureMessage);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
Memory somethingStarted = null;
|
||||
if (syncStarted != null) {
|
||||
other.synchronizeMailboxStarted(syncStarted.account, syncStarted.folderId);
|
||||
somethingStarted = syncStarted;
|
||||
}
|
||||
if (somethingStarted != null && somethingStarted.folderTotal > 0) {
|
||||
other.synchronizeMailboxProgress(somethingStarted.account, somethingStarted.folderId,
|
||||
somethingStarted.folderCompleted, somethingStarted.folderTotal);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void synchronizeMailboxStarted(Account account, long folderId) {
|
||||
Memory memory = getMemory(account, folderId);
|
||||
memory.syncingState = MemorizingState.STARTED;
|
||||
memory.folderCompleted = 0;
|
||||
memory.folderTotal = 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void synchronizeMailboxFinished(Account account, long folderId) {
|
||||
Memory memory = getMemory(account, folderId);
|
||||
memory.syncingState = MemorizingState.FINISHED;
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void synchronizeMailboxFailed(Account account, long folderId,
|
||||
String message) {
|
||||
|
||||
Memory memory = getMemory(account, folderId);
|
||||
memory.syncingState = MemorizingState.FAILED;
|
||||
memory.failureMessage = message;
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void synchronizeMailboxProgress(Account account, long folderId, int completed,
|
||||
int total) {
|
||||
Memory memory = getMemory(account, folderId);
|
||||
memory.folderCompleted = completed;
|
||||
memory.folderTotal = total;
|
||||
}
|
||||
|
||||
private Memory getMemory(Account account, long folderId) {
|
||||
Memory memory = memories.get(getMemoryKey(account, folderId));
|
||||
if (memory == null) {
|
||||
memory = new Memory(account, folderId);
|
||||
memories.put(getMemoryKey(memory.account, memory.folderId), memory);
|
||||
}
|
||||
return memory;
|
||||
}
|
||||
|
||||
private static String getMemoryKey(Account account, long folderId) {
|
||||
return account.getUuid() + ":" + folderId;
|
||||
}
|
||||
|
||||
private enum MemorizingState { STARTED, FINISHED, FAILED }
|
||||
|
||||
private static class Memory {
|
||||
Account account;
|
||||
long folderId;
|
||||
MemorizingState syncingState = null;
|
||||
String failureMessage = null;
|
||||
|
||||
int folderCompleted = 0;
|
||||
int folderTotal = 0;
|
||||
|
||||
Memory(Account account, long folderId) {
|
||||
this.account = account;
|
||||
this.folderId = folderId;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,76 @@
|
|||
package com.fsck.k9.controller
|
||||
|
||||
import com.fsck.k9.Account
|
||||
import com.fsck.k9.Preferences
|
||||
import com.fsck.k9.mailstore.MessageStoreManager
|
||||
import com.fsck.k9.search.ConditionsTreeNode
|
||||
import com.fsck.k9.search.LocalSearch
|
||||
import com.fsck.k9.search.SearchAccount
|
||||
import com.fsck.k9.search.excludeSpecialFolders
|
||||
import com.fsck.k9.search.getAccounts
|
||||
import com.fsck.k9.search.limitToDisplayableFolders
|
||||
import timber.log.Timber
|
||||
|
||||
interface MessageCountsProvider {
|
||||
fun getMessageCounts(account: Account): MessageCounts
|
||||
fun getMessageCounts(searchAccount: SearchAccount): MessageCounts
|
||||
fun getUnreadMessageCount(account: Account, folderId: Long): Int
|
||||
}
|
||||
|
||||
data class MessageCounts(val unread: Int, val starred: Int)
|
||||
|
||||
internal class DefaultMessageCountsProvider(
|
||||
private val preferences: Preferences,
|
||||
private val messageStoreManager: MessageStoreManager
|
||||
) : MessageCountsProvider {
|
||||
override fun getMessageCounts(account: Account): MessageCounts {
|
||||
val search = LocalSearch().apply {
|
||||
excludeSpecialFolders(account)
|
||||
limitToDisplayableFolders(account)
|
||||
}
|
||||
|
||||
return getMessageCounts(account, search.conditions)
|
||||
}
|
||||
|
||||
override fun getMessageCounts(searchAccount: SearchAccount): MessageCounts {
|
||||
val search = searchAccount.relatedSearch
|
||||
val accounts = search.getAccounts(preferences)
|
||||
|
||||
var unreadCount = 0
|
||||
var starredCount = 0
|
||||
for (account in accounts) {
|
||||
val accountMessageCount = getMessageCounts(account, search.conditions)
|
||||
unreadCount += accountMessageCount.unread
|
||||
starredCount += accountMessageCount.starred
|
||||
}
|
||||
|
||||
return MessageCounts(unreadCount, starredCount)
|
||||
}
|
||||
|
||||
override fun getUnreadMessageCount(account: Account, folderId: Long): Int {
|
||||
return try {
|
||||
val messageStore = messageStoreManager.getMessageStore(account)
|
||||
return if (folderId == account.outboxFolderId) {
|
||||
messageStore.getMessageCount(folderId)
|
||||
} else {
|
||||
messageStore.getUnreadMessageCount(folderId)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "Unable to getUnreadMessageCount for account: %s, folder: %d", account, folderId)
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
private fun getMessageCounts(account: Account, conditions: ConditionsTreeNode?): MessageCounts {
|
||||
return try {
|
||||
val messageStore = messageStoreManager.getMessageStore(account)
|
||||
return MessageCounts(
|
||||
unread = messageStore.getUnreadMessageCount(conditions),
|
||||
starred = messageStore.getStarredMessageCount(conditions)
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "Unable to getMessageCounts for account: %s", account)
|
||||
MessageCounts(unread = 0, starred = 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
package com.fsck.k9.controller
|
||||
|
||||
import com.fsck.k9.mail.filter.Base64
|
||||
import java.util.StringTokenizer
|
||||
|
||||
data class MessageReference(
|
||||
val accountUuid: String,
|
||||
val folderId: Long,
|
||||
val uid: String
|
||||
) {
|
||||
fun toIdentityString(): String {
|
||||
return buildString {
|
||||
append(IDENTITY_VERSION_2)
|
||||
append(IDENTITY_SEPARATOR)
|
||||
append(Base64.encode(accountUuid))
|
||||
append(IDENTITY_SEPARATOR)
|
||||
append(Base64.encode(folderId.toString()))
|
||||
append(IDENTITY_SEPARATOR)
|
||||
append(Base64.encode(uid))
|
||||
}
|
||||
}
|
||||
|
||||
fun equals(accountUuid: String, folderId: Long, uid: String): Boolean {
|
||||
return this.accountUuid == accountUuid && this.folderId == folderId && this.uid == uid
|
||||
}
|
||||
|
||||
fun withModifiedUid(newUid: String): MessageReference {
|
||||
return copy(uid = newUid)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val IDENTITY_VERSION_2 = '#'
|
||||
private const val IDENTITY_SEPARATOR = ":"
|
||||
|
||||
@JvmStatic
|
||||
fun parse(identity: String?): MessageReference? {
|
||||
if (identity == null || identity.isEmpty() || identity[0] != IDENTITY_VERSION_2) {
|
||||
return null
|
||||
}
|
||||
|
||||
val tokens = StringTokenizer(identity.substring(2), IDENTITY_SEPARATOR, false)
|
||||
if (tokens.countTokens() < 3) {
|
||||
return null
|
||||
}
|
||||
|
||||
val accountUuid = Base64.decode(tokens.nextToken())
|
||||
val folderId = Base64.decode(tokens.nextToken()).toLong()
|
||||
val uid = Base64.decode(tokens.nextToken())
|
||||
return MessageReference(accountUuid, folderId, uid)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
package com.fsck.k9.controller;
|
||||
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import timber.log.Timber;
|
||||
|
||||
|
||||
public class MessageReferenceHelper {
|
||||
public static List<MessageReference> toMessageReferenceList(List<String> messageReferenceStrings) {
|
||||
List<MessageReference> messageReferences = new ArrayList<>(messageReferenceStrings.size());
|
||||
for (String messageReferenceString : messageReferenceStrings) {
|
||||
MessageReference messageReference = MessageReference.parse(messageReferenceString);
|
||||
if (messageReference != null) {
|
||||
messageReferences.add(messageReference);
|
||||
} else {
|
||||
Timber.w("Invalid message reference: %s", messageReferenceString);
|
||||
}
|
||||
}
|
||||
|
||||
return messageReferences;
|
||||
}
|
||||
|
||||
public static ArrayList<String> toMessageReferenceStringList(List<MessageReference> messageReferences) {
|
||||
ArrayList<String> messageReferenceStrings = new ArrayList<>(messageReferences.size());
|
||||
for (MessageReference messageReference : messageReferences) {
|
||||
String messageReferenceString = messageReference.toIdentityString();
|
||||
messageReferenceStrings.add(messageReferenceString);
|
||||
}
|
||||
|
||||
return messageReferenceStrings;
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,269 @@
|
|||
package com.fsck.k9.controller;
|
||||
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import com.fsck.k9.Account;
|
||||
import com.fsck.k9.mail.Flag;
|
||||
import com.fsck.k9.mail.MessagingException;
|
||||
|
||||
import static com.fsck.k9.controller.Preconditions.requireNotNull;
|
||||
import static com.fsck.k9.controller.Preconditions.requireValidUids;
|
||||
|
||||
|
||||
public class MessagingControllerCommands {
|
||||
static final String COMMAND_APPEND = "append";
|
||||
static final String COMMAND_REPLACE = "replace";
|
||||
static final String COMMAND_MARK_ALL_AS_READ = "mark_all_as_read";
|
||||
static final String COMMAND_SET_FLAG = "set_flag";
|
||||
static final String COMMAND_DELETE = "delete";
|
||||
static final String COMMAND_EXPUNGE = "expunge";
|
||||
static final String COMMAND_MOVE_OR_COPY = "move_or_copy";
|
||||
static final String COMMAND_MOVE_AND_MARK_AS_READ = "move_and_mark_as_read";
|
||||
static final String COMMAND_EMPTY_TRASH = "empty_trash";
|
||||
|
||||
public abstract static class PendingCommand {
|
||||
public long databaseId;
|
||||
|
||||
|
||||
PendingCommand() { }
|
||||
|
||||
public abstract String getCommandName();
|
||||
public abstract void execute(MessagingController controller, Account account) throws MessagingException;
|
||||
}
|
||||
|
||||
public static class PendingMoveOrCopy extends PendingCommand {
|
||||
public final long srcFolderId;
|
||||
public final long destFolderId;
|
||||
public final boolean isCopy;
|
||||
public final List<String> uids;
|
||||
public final Map<String, String> newUidMap;
|
||||
|
||||
|
||||
public static PendingMoveOrCopy create(long srcFolderId, long destFolderId, boolean isCopy,
|
||||
Map<String, String> uidMap) {
|
||||
requireValidUids(uidMap);
|
||||
return new PendingMoveOrCopy(srcFolderId, destFolderId, isCopy, null, uidMap);
|
||||
}
|
||||
|
||||
private PendingMoveOrCopy(long srcFolderId, long destFolderId, boolean isCopy, List<String> uids,
|
||||
Map<String, String> newUidMap) {
|
||||
this.srcFolderId = srcFolderId;
|
||||
this.destFolderId = destFolderId;
|
||||
this.isCopy = isCopy;
|
||||
this.uids = uids;
|
||||
this.newUidMap = newUidMap;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getCommandName() {
|
||||
return COMMAND_MOVE_OR_COPY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute(MessagingController controller, Account account) throws MessagingException {
|
||||
controller.processPendingMoveOrCopy(this, account);
|
||||
}
|
||||
}
|
||||
|
||||
public static class PendingMoveAndMarkAsRead extends PendingCommand {
|
||||
public final long srcFolderId;
|
||||
public final long destFolderId;
|
||||
public final Map<String, String> newUidMap;
|
||||
|
||||
|
||||
public static PendingMoveAndMarkAsRead create(long srcFolderId, long destFolderId, Map<String, String> uidMap) {
|
||||
requireValidUids(uidMap);
|
||||
return new PendingMoveAndMarkAsRead(srcFolderId, destFolderId, uidMap);
|
||||
}
|
||||
|
||||
private PendingMoveAndMarkAsRead(long srcFolderId, long destFolderId, Map<String, String> newUidMap) {
|
||||
this.srcFolderId = srcFolderId;
|
||||
this.destFolderId = destFolderId;
|
||||
this.newUidMap = newUidMap;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getCommandName() {
|
||||
return COMMAND_MOVE_AND_MARK_AS_READ;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute(MessagingController controller, Account account) throws MessagingException {
|
||||
controller.processPendingMoveAndRead(this, account);
|
||||
}
|
||||
}
|
||||
|
||||
public static class PendingEmptyTrash extends PendingCommand {
|
||||
public static PendingEmptyTrash create() {
|
||||
return new PendingEmptyTrash();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getCommandName() {
|
||||
return COMMAND_EMPTY_TRASH;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute(MessagingController controller, Account account) throws MessagingException {
|
||||
controller.processPendingEmptyTrash(account);
|
||||
}
|
||||
}
|
||||
|
||||
public static class PendingSetFlag extends PendingCommand {
|
||||
public final long folderId;
|
||||
public final boolean newState;
|
||||
public final Flag flag;
|
||||
public final List<String> uids;
|
||||
|
||||
|
||||
public static PendingSetFlag create(long folderId, boolean newState, Flag flag, List<String> uids) {
|
||||
requireNotNull(flag);
|
||||
requireValidUids(uids);
|
||||
return new PendingSetFlag(folderId, newState, flag, uids);
|
||||
}
|
||||
|
||||
private PendingSetFlag(long folderId, boolean newState, Flag flag, List<String> uids) {
|
||||
this.folderId = folderId;
|
||||
this.newState = newState;
|
||||
this.flag = flag;
|
||||
this.uids = uids;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getCommandName() {
|
||||
return COMMAND_SET_FLAG;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute(MessagingController controller, Account account) throws MessagingException {
|
||||
controller.processPendingSetFlag(this, account);
|
||||
}
|
||||
}
|
||||
|
||||
public static class PendingAppend extends PendingCommand {
|
||||
public final long folderId;
|
||||
public final String uid;
|
||||
|
||||
|
||||
public static PendingAppend create(long folderId, String uid) {
|
||||
requireNotNull(uid);
|
||||
return new PendingAppend(folderId, uid);
|
||||
}
|
||||
|
||||
private PendingAppend(long folderId, String uid) {
|
||||
this.folderId = folderId;
|
||||
this.uid = uid;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getCommandName() {
|
||||
return COMMAND_APPEND;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute(MessagingController controller, Account account) throws MessagingException {
|
||||
controller.processPendingAppend(this, account);
|
||||
}
|
||||
}
|
||||
|
||||
public static class PendingReplace extends PendingCommand {
|
||||
public final long folderId;
|
||||
public final long uploadMessageId;
|
||||
public final long deleteMessageId;
|
||||
|
||||
|
||||
public static PendingReplace create(long folderId, long uploadMessageId, long deleteMessageId) {
|
||||
return new PendingReplace(folderId, uploadMessageId, deleteMessageId);
|
||||
}
|
||||
|
||||
private PendingReplace(long folderId, long uploadMessageId, long deleteMessageId) {
|
||||
this.folderId = folderId;
|
||||
this.uploadMessageId = uploadMessageId;
|
||||
this.deleteMessageId = deleteMessageId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getCommandName() {
|
||||
return COMMAND_REPLACE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute(MessagingController controller, Account account) throws MessagingException {
|
||||
controller.processPendingReplace(this, account);
|
||||
}
|
||||
}
|
||||
|
||||
public static class PendingMarkAllAsRead extends PendingCommand {
|
||||
public final long folderId;
|
||||
|
||||
|
||||
public static PendingMarkAllAsRead create(long folderId) {
|
||||
return new PendingMarkAllAsRead(folderId);
|
||||
}
|
||||
|
||||
private PendingMarkAllAsRead(long folderId) {
|
||||
this.folderId = folderId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getCommandName() {
|
||||
return COMMAND_MARK_ALL_AS_READ;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute(MessagingController controller, Account account) throws MessagingException {
|
||||
controller.processPendingMarkAllAsRead(this, account);
|
||||
}
|
||||
}
|
||||
|
||||
public static class PendingDelete extends PendingCommand {
|
||||
public final long folderId;
|
||||
public final List<String> uids;
|
||||
|
||||
|
||||
public static PendingDelete create(long folderId, List<String> uids) {
|
||||
requireValidUids(uids);
|
||||
return new PendingDelete(folderId, uids);
|
||||
}
|
||||
|
||||
private PendingDelete(long folderId, List<String> uids) {
|
||||
this.folderId = folderId;
|
||||
this.uids = uids;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getCommandName() {
|
||||
return COMMAND_DELETE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute(MessagingController controller, Account account) throws MessagingException {
|
||||
controller.processPendingDelete(this, account);
|
||||
}
|
||||
}
|
||||
|
||||
public static class PendingExpunge extends PendingCommand {
|
||||
public final long folderId;
|
||||
|
||||
|
||||
public static PendingExpunge create(long folderId) {
|
||||
return new PendingExpunge(folderId);
|
||||
}
|
||||
|
||||
private PendingExpunge(long folderId) {
|
||||
this.folderId = folderId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getCommandName() {
|
||||
return COMMAND_EXPUNGE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute(MessagingController controller, Account account) throws MessagingException {
|
||||
controller.processPendingExpunge(this, account);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
|
||||
package com.fsck.k9.controller;
|
||||
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import com.fsck.k9.Account;
|
||||
import com.fsck.k9.mail.Message;
|
||||
import com.fsck.k9.mail.Part;
|
||||
import com.fsck.k9.mailstore.LocalMessage;
|
||||
|
||||
|
||||
public interface MessagingListener {
|
||||
void synchronizeMailboxStarted(Account account, long folderId);
|
||||
void synchronizeMailboxHeadersStarted(Account account, String folderServerId);
|
||||
void synchronizeMailboxHeadersProgress(Account account, String folderServerId, int completed, int total);
|
||||
void synchronizeMailboxHeadersFinished(Account account, String folderServerId, int totalMessagesInMailbox,
|
||||
int numNewMessages);
|
||||
void synchronizeMailboxProgress(Account account, long folderId, int completed, int total);
|
||||
void synchronizeMailboxNewMessage(Account account, String folderServerId, Message message);
|
||||
void synchronizeMailboxRemovedMessage(Account account, String folderServerId, String messageServerId);
|
||||
void synchronizeMailboxFinished(Account account, long folderId);
|
||||
void synchronizeMailboxFailed(Account account, long folderId, String message);
|
||||
|
||||
void loadMessageRemoteFinished(Account account, long folderId, String uid);
|
||||
void loadMessageRemoteFailed(Account account, long folderId, String uid, Throwable t);
|
||||
|
||||
void checkMailStarted(Context context, Account account);
|
||||
void checkMailFinished(Context context, Account account);
|
||||
|
||||
void folderStatusChanged(Account account, long folderId);
|
||||
|
||||
void messageUidChanged(Account account, long folderId, String oldUid, String newUid);
|
||||
|
||||
void loadAttachmentFinished(Account account, Message message, Part part);
|
||||
void loadAttachmentFailed(Account account, Message message, Part part, String reason);
|
||||
|
||||
void remoteSearchStarted(long folderId);
|
||||
void remoteSearchServerQueryComplete(long folderId, int numResults, int maxResults);
|
||||
void remoteSearchFinished(long folderId, int numResults, int maxResults, List<String> extraResults);
|
||||
void remoteSearchFailed(String folderServerId, String err);
|
||||
|
||||
void enableProgressIndicator(boolean enable);
|
||||
|
||||
void updateProgress(int progress);
|
||||
}
|
||||
|
|
@ -0,0 +1,63 @@
|
|||
package com.fsck.k9.controller
|
||||
|
||||
import com.fsck.k9.Account
|
||||
import com.fsck.k9.Preferences
|
||||
import com.fsck.k9.mailstore.MessageStoreManager
|
||||
import com.fsck.k9.notification.NotificationController
|
||||
import com.fsck.k9.search.LocalSearch
|
||||
import com.fsck.k9.search.isNewMessages
|
||||
import com.fsck.k9.search.isSingleFolder
|
||||
import com.fsck.k9.search.isUnifiedInbox
|
||||
|
||||
internal class NotificationOperations(
|
||||
private val notificationController: NotificationController,
|
||||
private val preferences: Preferences,
|
||||
private val messageStoreManager: MessageStoreManager
|
||||
) {
|
||||
fun clearNotifications(search: LocalSearch) {
|
||||
if (search.isUnifiedInbox) {
|
||||
clearUnifiedInboxNotifications()
|
||||
} else if (search.isNewMessages) {
|
||||
clearAllNotifications()
|
||||
} else if (search.isSingleFolder) {
|
||||
val account = search.firstAccount() ?: return
|
||||
val folderId = search.folderIds.first()
|
||||
clearNotifications(account, folderId)
|
||||
} else {
|
||||
// TODO: Remove notifications when updating the message list. That way we can easily remove only
|
||||
// notifications for messages that are currently displayed in the list.
|
||||
}
|
||||
}
|
||||
|
||||
private fun clearUnifiedInboxNotifications() {
|
||||
for (account in preferences.accounts) {
|
||||
val messageStore = messageStoreManager.getMessageStore(account)
|
||||
|
||||
val folderIds = messageStore.getFolders(excludeLocalOnly = true) { folderDetails ->
|
||||
if (folderDetails.isIntegrate) folderDetails.id else null
|
||||
}.filterNotNull().toSet()
|
||||
|
||||
if (folderIds.isNotEmpty()) {
|
||||
notificationController.clearNewMailNotifications(account) { messageReferences ->
|
||||
messageReferences.filter { messageReference -> messageReference.folderId in folderIds }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun clearAllNotifications() {
|
||||
for (account in preferences.accounts) {
|
||||
notificationController.clearNewMailNotifications(account, clearNewMessageState = false)
|
||||
}
|
||||
}
|
||||
|
||||
private fun clearNotifications(account: Account, folderId: Long) {
|
||||
notificationController.clearNewMailNotifications(account) { messageReferences ->
|
||||
messageReferences.filter { messageReference -> messageReference.folderId == folderId }
|
||||
}
|
||||
}
|
||||
|
||||
private fun LocalSearch.firstAccount(): Account? {
|
||||
return preferences.getAccount(accountUuids.first())
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
package com.fsck.k9.controller
|
||||
|
||||
class NotificationState {
|
||||
@get:JvmName("wasNotified")
|
||||
var wasNotified: Boolean = false
|
||||
}
|
||||
|
|
@ -0,0 +1,77 @@
|
|||
package com.fsck.k9.controller;
|
||||
|
||||
|
||||
import java.io.IOError;
|
||||
import java.io.IOException;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import com.fsck.k9.controller.MessagingControllerCommands.PendingAppend;
|
||||
import com.fsck.k9.controller.MessagingControllerCommands.PendingCommand;
|
||||
import com.fsck.k9.controller.MessagingControllerCommands.PendingDelete;
|
||||
import com.fsck.k9.controller.MessagingControllerCommands.PendingEmptyTrash;
|
||||
import com.fsck.k9.controller.MessagingControllerCommands.PendingExpunge;
|
||||
import com.fsck.k9.controller.MessagingControllerCommands.PendingMarkAllAsRead;
|
||||
import com.fsck.k9.controller.MessagingControllerCommands.PendingMoveAndMarkAsRead;
|
||||
import com.fsck.k9.controller.MessagingControllerCommands.PendingMoveOrCopy;
|
||||
import com.fsck.k9.controller.MessagingControllerCommands.PendingReplace;
|
||||
import com.fsck.k9.controller.MessagingControllerCommands.PendingSetFlag;
|
||||
import com.squareup.moshi.JsonAdapter;
|
||||
import com.squareup.moshi.Moshi;
|
||||
|
||||
|
||||
public class PendingCommandSerializer {
|
||||
private static final PendingCommandSerializer INSTANCE = new PendingCommandSerializer();
|
||||
|
||||
|
||||
private final Map<String, JsonAdapter<? extends PendingCommand>> adapters;
|
||||
|
||||
|
||||
private PendingCommandSerializer() {
|
||||
Moshi moshi = new Moshi.Builder().build();
|
||||
HashMap<String, JsonAdapter<? extends PendingCommand>> adapters = new HashMap<>();
|
||||
|
||||
adapters.put(MessagingControllerCommands.COMMAND_MOVE_OR_COPY, moshi.adapter(PendingMoveOrCopy.class));
|
||||
adapters.put(MessagingControllerCommands.COMMAND_MOVE_AND_MARK_AS_READ,
|
||||
moshi.adapter(PendingMoveAndMarkAsRead.class));
|
||||
adapters.put(MessagingControllerCommands.COMMAND_APPEND, moshi.adapter(PendingAppend.class));
|
||||
adapters.put(MessagingControllerCommands.COMMAND_REPLACE, moshi.adapter(PendingReplace.class));
|
||||
adapters.put(MessagingControllerCommands.COMMAND_EMPTY_TRASH, moshi.adapter(PendingEmptyTrash.class));
|
||||
adapters.put(MessagingControllerCommands.COMMAND_EXPUNGE, moshi.adapter(PendingExpunge.class));
|
||||
adapters.put(MessagingControllerCommands.COMMAND_MARK_ALL_AS_READ, moshi.adapter(PendingMarkAllAsRead.class));
|
||||
adapters.put(MessagingControllerCommands.COMMAND_SET_FLAG, moshi.adapter(PendingSetFlag.class));
|
||||
adapters.put(MessagingControllerCommands.COMMAND_DELETE, moshi.adapter(PendingDelete.class));
|
||||
|
||||
this.adapters = Collections.unmodifiableMap(adapters);
|
||||
}
|
||||
|
||||
|
||||
public static PendingCommandSerializer getInstance() {
|
||||
return INSTANCE;
|
||||
}
|
||||
|
||||
|
||||
public <T extends PendingCommand> String serialize(T command) {
|
||||
// noinspection unchecked, we know the map has correctly matching adapters
|
||||
JsonAdapter<T> adapter = (JsonAdapter<T>) adapters.get(command.getCommandName());
|
||||
if (adapter == null) {
|
||||
throw new IllegalArgumentException("Unsupported pending command type!");
|
||||
}
|
||||
return adapter.toJson(command);
|
||||
}
|
||||
|
||||
public PendingCommand unserialize(long databaseId, String commandName, String data) {
|
||||
JsonAdapter<? extends PendingCommand> adapter = adapters.get(commandName);
|
||||
if (adapter == null) {
|
||||
throw new IllegalArgumentException("Unsupported pending command type!");
|
||||
}
|
||||
try {
|
||||
PendingCommand command = adapter.fromJson(data);
|
||||
command.databaseId = databaseId;
|
||||
return command;
|
||||
} catch (IOException e) {
|
||||
throw new IOError(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
@file:JvmName("Preconditions")
|
||||
|
||||
package com.fsck.k9.controller
|
||||
|
||||
import com.fsck.k9.K9
|
||||
|
||||
fun <T : Any> requireNotNull(value: T?) {
|
||||
kotlin.requireNotNull(value)
|
||||
}
|
||||
|
||||
fun requireValidUids(uidMap: Map<String?, String?>?) {
|
||||
kotlin.requireNotNull(uidMap)
|
||||
for ((sourceUid, destinationUid) in uidMap) {
|
||||
requireNotLocalUid(sourceUid)
|
||||
kotlin.requireNotNull(destinationUid)
|
||||
}
|
||||
}
|
||||
|
||||
fun requireValidUids(uids: List<String?>?) {
|
||||
kotlin.requireNotNull(uids)
|
||||
for (uid in uids) {
|
||||
requireNotLocalUid(uid)
|
||||
}
|
||||
}
|
||||
|
||||
private fun requireNotLocalUid(uid: String?) {
|
||||
kotlin.requireNotNull(uid)
|
||||
require(!uid.startsWith(K9.LOCAL_UID_PREFIX)) { "Local UID found: $uid" }
|
||||
}
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
package com.fsck.k9.controller;
|
||||
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.util.Timer;
|
||||
import java.util.TimerTask;
|
||||
|
||||
import com.fsck.k9.mail.DefaultBodyFactory;
|
||||
import org.apache.commons.io.output.CountingOutputStream;
|
||||
|
||||
|
||||
class ProgressBodyFactory extends DefaultBodyFactory {
|
||||
private final ProgressListener progressListener;
|
||||
|
||||
|
||||
ProgressBodyFactory(ProgressListener progressListener) {
|
||||
this.progressListener = progressListener;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void copyData(InputStream inputStream, OutputStream outputStream) throws IOException {
|
||||
Timer timer = new Timer();
|
||||
try (CountingOutputStream countingOutputStream = new CountingOutputStream(outputStream)) {
|
||||
timer.scheduleAtFixedRate(new TimerTask() {
|
||||
@Override
|
||||
public void run() {
|
||||
progressListener.updateProgress(countingOutputStream.getCount());
|
||||
}
|
||||
}, 0, 50);
|
||||
|
||||
super.copyData(inputStream, countingOutputStream);
|
||||
} finally {
|
||||
timer.cancel();
|
||||
}
|
||||
}
|
||||
|
||||
interface ProgressListener {
|
||||
void updateProgress(int progress);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,108 @@
|
|||
|
||||
package com.fsck.k9.controller;
|
||||
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import com.fsck.k9.Account;
|
||||
import com.fsck.k9.mail.Message;
|
||||
import com.fsck.k9.mail.Part;
|
||||
|
||||
|
||||
public abstract class SimpleMessagingListener implements MessagingListener {
|
||||
@Override
|
||||
public void synchronizeMailboxStarted(Account account, long folderId) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void synchronizeMailboxHeadersStarted(Account account, String folderServerId) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void synchronizeMailboxHeadersProgress(Account account, String folderServerId, int completed, int total) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void synchronizeMailboxHeadersFinished(Account account, String folderServerId, int totalMessagesInMailbox,
|
||||
int numNewMessages) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void synchronizeMailboxProgress(Account account, long folderId, int completed, int total) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void synchronizeMailboxNewMessage(Account account, String folderServerId, Message message) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void synchronizeMailboxRemovedMessage(Account account, String folderServerId, String messageServerId) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void synchronizeMailboxFinished(Account account, long folderId) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void synchronizeMailboxFailed(Account account, long folderId, String message) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void loadMessageRemoteFinished(Account account, long folderId, String uid) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void loadMessageRemoteFailed(Account account, long folderId, String uid, Throwable t) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void checkMailStarted(Context context, Account account) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void checkMailFinished(Context context, Account account) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void folderStatusChanged(Account account, long folderId) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void messageUidChanged(Account account, long folderId, String oldUid, String newUid) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void loadAttachmentFinished(Account account, Message message, Part part) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void loadAttachmentFailed(Account account, Message message, Part part, String reason) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void remoteSearchStarted(long folderId) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void remoteSearchServerQueryComplete(long folderId, int numResults, int maxResults) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void remoteSearchFinished(long folderId, int numResults, int maxResults, List<String> extraResults) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void remoteSearchFailed(String folderServerId, String err) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void enableProgressIndicator(boolean enable) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateProgress(int progress) {
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
package com.fsck.k9.controller;
|
||||
|
||||
|
||||
import java.util.Comparator;
|
||||
|
||||
import com.fsck.k9.mail.Message;
|
||||
|
||||
|
||||
public class UidReverseComparator implements Comparator<Message> {
|
||||
@Override
|
||||
public int compare(Message messageLeft, Message messageRight) {
|
||||
Long uidLeft = getUidForMessage(messageLeft);
|
||||
Long uidRight = getUidForMessage(messageRight);
|
||||
|
||||
if (uidLeft == null && uidRight == null) {
|
||||
return 0;
|
||||
} else if (uidLeft == null) {
|
||||
return 1;
|
||||
} else if (uidRight == null) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
// reverse order
|
||||
return uidRight.compareTo(uidLeft);
|
||||
}
|
||||
|
||||
private Long getUidForMessage(Message message) {
|
||||
try {
|
||||
return Long.parseLong(message.getUid());
|
||||
} catch (NullPointerException | NumberFormatException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,103 @@
|
|||
package com.fsck.k9.controller.push
|
||||
|
||||
import com.fsck.k9.Account
|
||||
import com.fsck.k9.Account.FolderMode
|
||||
import com.fsck.k9.Preferences
|
||||
import com.fsck.k9.backend.BackendManager
|
||||
import com.fsck.k9.backend.api.BackendPusher
|
||||
import com.fsck.k9.backend.api.BackendPusherCallback
|
||||
import com.fsck.k9.controller.MessagingController
|
||||
import com.fsck.k9.mailstore.FolderRepository
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
|
||||
internal class AccountPushController(
|
||||
private val backendManager: BackendManager,
|
||||
private val messagingController: MessagingController,
|
||||
private val preferences: Preferences,
|
||||
private val folderRepository: FolderRepository,
|
||||
backgroundDispatcher: CoroutineDispatcher = Dispatchers.IO,
|
||||
private val account: Account
|
||||
) {
|
||||
private val coroutineScope = CoroutineScope(backgroundDispatcher)
|
||||
|
||||
@Volatile
|
||||
private var backendPusher: BackendPusher? = null
|
||||
|
||||
private val backendPusherCallback = object : BackendPusherCallback {
|
||||
override fun onPushEvent(folderServerId: String) {
|
||||
syncFolders(folderServerId)
|
||||
}
|
||||
|
||||
override fun onPushError(exception: Exception) {
|
||||
messagingController.handleException(account, exception)
|
||||
}
|
||||
|
||||
override fun onPushNotSupported() {
|
||||
Timber.v("AccountPushController(%s) - Push not supported. Disabling Push for account.", account.uuid)
|
||||
disablePush()
|
||||
}
|
||||
}
|
||||
|
||||
fun start() {
|
||||
Timber.v("AccountPushController(%s).start()", account.uuid)
|
||||
startBackendPusher()
|
||||
startListeningForPushFolders()
|
||||
}
|
||||
|
||||
fun stop() {
|
||||
Timber.v("AccountPushController(%s).stop()", account.uuid)
|
||||
stopListeningForPushFolders()
|
||||
stopBackendPusher()
|
||||
}
|
||||
|
||||
fun reconnect() {
|
||||
Timber.v("AccountPushController(%s).reconnect()", account.uuid)
|
||||
backendPusher?.reconnect()
|
||||
}
|
||||
|
||||
private fun startBackendPusher() {
|
||||
val backend = backendManager.getBackend(account)
|
||||
backendPusher = backend.createPusher(backendPusherCallback).also { backendPusher ->
|
||||
backendPusher.start()
|
||||
}
|
||||
}
|
||||
|
||||
private fun stopBackendPusher() {
|
||||
backendPusher?.stop()
|
||||
backendPusher = null
|
||||
}
|
||||
|
||||
private fun startListeningForPushFolders() {
|
||||
coroutineScope.launch {
|
||||
folderRepository.getPushFoldersFlow(account).collect { remoteFolders ->
|
||||
val folderServerIds = remoteFolders.map { it.serverId }
|
||||
updatePushFolders(folderServerIds)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun stopListeningForPushFolders() {
|
||||
coroutineScope.cancel()
|
||||
}
|
||||
|
||||
private fun updatePushFolders(folderServerIds: List<String>) {
|
||||
Timber.v("AccountPushController(%s).updatePushFolders(): %s", account.uuid, folderServerIds)
|
||||
|
||||
backendPusher?.updateFolders(folderServerIds)
|
||||
}
|
||||
|
||||
private fun syncFolders(folderServerId: String) {
|
||||
messagingController.synchronizeMailboxBlocking(account, folderServerId)
|
||||
}
|
||||
|
||||
private fun disablePush() {
|
||||
account.folderPushMode = FolderMode.NONE
|
||||
preferences.saveAccount(account)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
package com.fsck.k9.controller.push
|
||||
|
||||
import com.fsck.k9.Account
|
||||
import com.fsck.k9.Preferences
|
||||
import com.fsck.k9.backend.BackendManager
|
||||
import com.fsck.k9.controller.MessagingController
|
||||
import com.fsck.k9.mailstore.FolderRepository
|
||||
|
||||
internal class AccountPushControllerFactory(
|
||||
private val backendManager: BackendManager,
|
||||
private val messagingController: MessagingController,
|
||||
private val folderRepository: FolderRepository,
|
||||
private val preferences: Preferences
|
||||
) {
|
||||
fun create(account: Account): AccountPushController {
|
||||
return AccountPushController(
|
||||
backendManager,
|
||||
messagingController,
|
||||
preferences,
|
||||
folderRepository,
|
||||
account = account
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
package com.fsck.k9.controller.push
|
||||
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.ContentResolver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import com.fsck.k9.K9
|
||||
import timber.log.Timber
|
||||
|
||||
/**
|
||||
* Listen for changes to the system's auto sync setting.
|
||||
*/
|
||||
internal class AutoSyncManager(private val context: Context) {
|
||||
val isAutoSyncDisabled: Boolean
|
||||
get() = respectSystemAutoSync && !ContentResolver.getMasterSyncAutomatically()
|
||||
|
||||
val respectSystemAutoSync: Boolean
|
||||
get() = K9.backgroundOps == K9.BACKGROUND_OPS.WHEN_CHECKED_AUTO_SYNC
|
||||
|
||||
private var isRegistered = false
|
||||
private var listener: AutoSyncListener? = null
|
||||
|
||||
private val intentFilter = IntentFilter().apply {
|
||||
addAction("com.android.sync.SYNC_CONN_STATUS_CHANGED")
|
||||
}
|
||||
|
||||
private val receiver = object : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context, intent: Intent?) {
|
||||
val listener = synchronized(this@AutoSyncManager) { listener }
|
||||
listener?.onAutoSyncChanged()
|
||||
}
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun registerListener(listener: AutoSyncListener) {
|
||||
if (!isRegistered) {
|
||||
Timber.v("Registering auto sync listener")
|
||||
isRegistered = true
|
||||
this.listener = listener
|
||||
context.registerReceiver(receiver, intentFilter)
|
||||
}
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun unregisterListener() {
|
||||
if (isRegistered) {
|
||||
Timber.v("Unregistering auto sync listener")
|
||||
isRegistered = false
|
||||
context.unregisterReceiver(receiver)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal fun interface AutoSyncListener {
|
||||
fun onAutoSyncChanged()
|
||||
}
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
package com.fsck.k9.controller.push
|
||||
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager.COMPONENT_ENABLED_STATE_DISABLED
|
||||
import android.content.pm.PackageManager.COMPONENT_ENABLED_STATE_ENABLED
|
||||
import android.content.pm.PackageManager.DONT_KILL_APP
|
||||
import java.lang.Exception
|
||||
import org.koin.core.component.KoinComponent
|
||||
import org.koin.core.component.inject
|
||||
import timber.log.Timber
|
||||
|
||||
class BootCompleteReceiver : BroadcastReceiver(), KoinComponent {
|
||||
private val pushController: PushController by inject()
|
||||
|
||||
override fun onReceive(context: Context, intent: Intent?) {
|
||||
Timber.v("BootCompleteReceiver.onReceive()")
|
||||
|
||||
pushController.init()
|
||||
}
|
||||
}
|
||||
|
||||
class BootCompleteManager(context: Context) {
|
||||
private val packageManager = context.packageManager
|
||||
private val componentName = ComponentName(context, BootCompleteReceiver::class.java)
|
||||
|
||||
fun enableReceiver() {
|
||||
Timber.v("Enable BootCompleteReceiver")
|
||||
try {
|
||||
packageManager.setComponentEnabledSetting(componentName, COMPONENT_ENABLED_STATE_ENABLED, DONT_KILL_APP)
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "Error enabling BootCompleteReceiver")
|
||||
}
|
||||
}
|
||||
|
||||
fun disableReceiver() {
|
||||
Timber.v("Disable BootCompleteReceiver")
|
||||
try {
|
||||
packageManager.setComponentEnabledSetting(componentName, COMPONENT_ENABLED_STATE_DISABLED, DONT_KILL_APP)
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "Error disabling BootCompleteReceiver")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
package com.fsck.k9.controller.push
|
||||
|
||||
import org.koin.dsl.module
|
||||
|
||||
internal val controllerPushModule = module {
|
||||
single { PushServiceManager(context = get()) }
|
||||
single { BootCompleteManager(context = get()) }
|
||||
single { AutoSyncManager(context = get()) }
|
||||
single {
|
||||
AccountPushControllerFactory(
|
||||
backendManager = get(),
|
||||
messagingController = get(),
|
||||
folderRepository = get(),
|
||||
preferences = get()
|
||||
)
|
||||
}
|
||||
single {
|
||||
PushController(
|
||||
preferences = get(),
|
||||
generalSettingsManager = get(),
|
||||
backendManager = get(),
|
||||
pushServiceManager = get(),
|
||||
bootCompleteManager = get(),
|
||||
autoSyncManager = get(),
|
||||
pushNotificationManager = get(),
|
||||
connectivityManager = get(),
|
||||
accountPushControllerFactory = get()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,252 @@
|
|||
package com.fsck.k9.controller.push
|
||||
|
||||
import com.fsck.k9.Account
|
||||
import com.fsck.k9.Account.FolderMode
|
||||
import com.fsck.k9.Preferences
|
||||
import com.fsck.k9.backend.BackendManager
|
||||
import com.fsck.k9.network.ConnectivityChangeListener
|
||||
import com.fsck.k9.network.ConnectivityManager
|
||||
import com.fsck.k9.notification.PushNotificationManager
|
||||
import com.fsck.k9.notification.PushNotificationState
|
||||
import com.fsck.k9.notification.PushNotificationState.LISTENING
|
||||
import com.fsck.k9.notification.PushNotificationState.WAIT_BACKGROUND_SYNC
|
||||
import com.fsck.k9.notification.PushNotificationState.WAIT_NETWORK
|
||||
import com.fsck.k9.preferences.BackgroundSync
|
||||
import com.fsck.k9.preferences.GeneralSettingsManager
|
||||
import java.util.concurrent.Executors
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.asCoroutineDispatcher
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
|
||||
/**
|
||||
* Starts and stops [AccountPushController]s as necessary. Manages the Push foreground service.
|
||||
*/
|
||||
class PushController internal constructor(
|
||||
private val preferences: Preferences,
|
||||
private val generalSettingsManager: GeneralSettingsManager,
|
||||
private val backendManager: BackendManager,
|
||||
private val pushServiceManager: PushServiceManager,
|
||||
private val bootCompleteManager: BootCompleteManager,
|
||||
private val autoSyncManager: AutoSyncManager,
|
||||
private val pushNotificationManager: PushNotificationManager,
|
||||
private val connectivityManager: ConnectivityManager,
|
||||
private val accountPushControllerFactory: AccountPushControllerFactory,
|
||||
private val coroutineScope: CoroutineScope = GlobalScope,
|
||||
private val coroutineDispatcher: CoroutineDispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher()
|
||||
) {
|
||||
private val lock = Any()
|
||||
private var initializationStarted = false
|
||||
private val pushers = mutableMapOf<String, AccountPushController>()
|
||||
|
||||
private val autoSyncListener = AutoSyncListener(::onAutoSyncChanged)
|
||||
private val connectivityChangeListener = object : ConnectivityChangeListener {
|
||||
override fun onConnectivityChanged() = this@PushController.onConnectivityChanged()
|
||||
override fun onConnectivityLost() = this@PushController.onConnectivityLost()
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize [PushController].
|
||||
*
|
||||
* Only call this method in situations where starting a foreground service is allowed.
|
||||
* See https://developer.android.com/about/versions/12/foreground-services
|
||||
*/
|
||||
fun init() {
|
||||
synchronized(lock) {
|
||||
if (initializationStarted) {
|
||||
return
|
||||
}
|
||||
initializationStarted = true
|
||||
}
|
||||
|
||||
coroutineScope.launch(coroutineDispatcher) {
|
||||
initInBackground()
|
||||
}
|
||||
}
|
||||
|
||||
fun disablePush() {
|
||||
Timber.v("PushController.disablePush()")
|
||||
|
||||
coroutineScope.launch(coroutineDispatcher) {
|
||||
for (account in preferences.accounts) {
|
||||
account.folderPushMode = FolderMode.NONE
|
||||
preferences.saveAccount(account)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun initInBackground() {
|
||||
Timber.v("PushController.initInBackground()")
|
||||
|
||||
preferences.addOnAccountsChangeListener(::onAccountsChanged)
|
||||
listenForBackgroundSyncChanges()
|
||||
backendManager.addListener(::onBackendChanged)
|
||||
|
||||
updatePushers()
|
||||
}
|
||||
|
||||
private fun listenForBackgroundSyncChanges() {
|
||||
generalSettingsManager.getSettingsFlow()
|
||||
.map { it.backgroundSync }
|
||||
.distinctUntilChanged()
|
||||
.onEach {
|
||||
launchUpdatePushers()
|
||||
}
|
||||
.launchIn(coroutineScope)
|
||||
}
|
||||
|
||||
private fun onAccountsChanged() {
|
||||
launchUpdatePushers()
|
||||
}
|
||||
|
||||
private fun onAutoSyncChanged() {
|
||||
launchUpdatePushers()
|
||||
}
|
||||
|
||||
private fun onConnectivityChanged() {
|
||||
coroutineScope.launch(coroutineDispatcher) {
|
||||
synchronized(lock) {
|
||||
for (accountPushController in pushers.values) {
|
||||
accountPushController.reconnect()
|
||||
}
|
||||
}
|
||||
|
||||
updatePushers()
|
||||
}
|
||||
}
|
||||
|
||||
private fun onConnectivityLost() {
|
||||
launchUpdatePushers()
|
||||
}
|
||||
|
||||
private fun onBackendChanged(account: Account) {
|
||||
coroutineScope.launch(coroutineDispatcher) {
|
||||
val accountPushController = synchronized(lock) {
|
||||
pushers.remove(account.uuid)
|
||||
}
|
||||
|
||||
accountPushController?.stop()
|
||||
updatePushers()
|
||||
}
|
||||
}
|
||||
|
||||
private fun launchUpdatePushers() {
|
||||
coroutineScope.launch(coroutineDispatcher) {
|
||||
updatePushers()
|
||||
}
|
||||
}
|
||||
|
||||
private fun updatePushers() {
|
||||
Timber.v("PushController.updatePushers()")
|
||||
|
||||
val generalSettings = generalSettingsManager.getSettings()
|
||||
|
||||
val backgroundSyncDisabledViaSystem = autoSyncManager.isAutoSyncDisabled
|
||||
val backgroundSyncDisabledInApp = generalSettings.backgroundSync == BackgroundSync.NEVER
|
||||
val networkNotAvailable = !connectivityManager.isNetworkAvailable()
|
||||
val realPushAccounts = getPushAccounts()
|
||||
|
||||
val pushAccounts = if (backgroundSyncDisabledViaSystem || backgroundSyncDisabledInApp || networkNotAvailable) {
|
||||
emptyList()
|
||||
} else {
|
||||
realPushAccounts
|
||||
}
|
||||
val pushAccountUuids = pushAccounts.map { it.uuid }
|
||||
|
||||
val arePushersActive = synchronized(lock) {
|
||||
val currentPushAccountUuids = pushers.keys
|
||||
val startPushAccountUuids = pushAccountUuids - currentPushAccountUuids
|
||||
val stopPushAccountUuids = currentPushAccountUuids - pushAccountUuids
|
||||
|
||||
if (stopPushAccountUuids.isNotEmpty()) {
|
||||
Timber.v("..Stopping PushController for accounts: %s", stopPushAccountUuids)
|
||||
for (accountUuid in stopPushAccountUuids) {
|
||||
val accountPushController = pushers.remove(accountUuid)
|
||||
accountPushController?.stop()
|
||||
}
|
||||
}
|
||||
|
||||
if (startPushAccountUuids.isNotEmpty()) {
|
||||
Timber.v("..Starting PushController for accounts: %s", startPushAccountUuids)
|
||||
for (accountUuid in startPushAccountUuids) {
|
||||
val account = preferences.getAccount(accountUuid) ?: error("Account not found: $accountUuid")
|
||||
pushers[accountUuid] = accountPushControllerFactory.create(account).also { accountPushController ->
|
||||
accountPushController.start()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Timber.v("..Running PushControllers: %s", pushers.keys)
|
||||
|
||||
pushers.isNotEmpty()
|
||||
}
|
||||
|
||||
when {
|
||||
realPushAccounts.isEmpty() -> {
|
||||
stopServices()
|
||||
}
|
||||
backgroundSyncDisabledViaSystem -> {
|
||||
setPushNotificationState(WAIT_BACKGROUND_SYNC)
|
||||
startServices()
|
||||
}
|
||||
networkNotAvailable -> {
|
||||
setPushNotificationState(WAIT_NETWORK)
|
||||
startServices()
|
||||
}
|
||||
arePushersActive -> {
|
||||
setPushNotificationState(LISTENING)
|
||||
startServices()
|
||||
}
|
||||
else -> {
|
||||
stopServices()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getPushAccounts(): List<Account> {
|
||||
return preferences.accounts.filter { account ->
|
||||
account.folderPushMode != FolderMode.NONE && backendManager.getBackend(account).isPushCapable
|
||||
}
|
||||
}
|
||||
private fun setPushNotificationState(notificationState: PushNotificationState) {
|
||||
pushNotificationManager.notificationState = notificationState
|
||||
}
|
||||
|
||||
private fun startServices() {
|
||||
pushServiceManager.start()
|
||||
bootCompleteManager.enableReceiver()
|
||||
registerAutoSyncListener()
|
||||
registerConnectivityChangeListener()
|
||||
connectivityManager.start()
|
||||
}
|
||||
|
||||
private fun stopServices() {
|
||||
pushServiceManager.stop()
|
||||
bootCompleteManager.disableReceiver()
|
||||
autoSyncManager.unregisterListener()
|
||||
unregisterConnectivityChangeListener()
|
||||
connectivityManager.stop()
|
||||
}
|
||||
|
||||
private fun registerAutoSyncListener() {
|
||||
if (autoSyncManager.respectSystemAutoSync) {
|
||||
autoSyncManager.registerListener(autoSyncListener)
|
||||
} else {
|
||||
autoSyncManager.unregisterListener()
|
||||
}
|
||||
}
|
||||
|
||||
private fun registerConnectivityChangeListener() {
|
||||
connectivityManager.addListener(connectivityChangeListener)
|
||||
}
|
||||
|
||||
private fun unregisterConnectivityChangeListener() {
|
||||
connectivityManager.removeListener(connectivityChangeListener)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
package com.fsck.k9.controller.push
|
||||
|
||||
import android.app.Service
|
||||
import android.content.Intent
|
||||
import android.content.pm.ServiceInfo
|
||||
import android.os.Build
|
||||
import android.os.IBinder
|
||||
import com.fsck.k9.notification.PushNotificationManager
|
||||
import org.koin.android.ext.android.inject
|
||||
import timber.log.Timber
|
||||
|
||||
/**
|
||||
* Foreground service that is used to keep the app alive while listening for new emails (Push).
|
||||
*/
|
||||
class PushService : Service() {
|
||||
private val pushNotificationManager: PushNotificationManager by inject()
|
||||
private val pushController: PushController by inject()
|
||||
|
||||
override fun onCreate() {
|
||||
Timber.v("PushService.onCreate()")
|
||||
super.onCreate()
|
||||
}
|
||||
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
Timber.v("PushService.onStartCommand()")
|
||||
super.onStartCommand(intent, flags, startId)
|
||||
|
||||
startForeground()
|
||||
initializePushController()
|
||||
|
||||
return START_STICKY
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
Timber.v("PushService.onDestroy()")
|
||||
pushNotificationManager.setForegroundServiceStopped()
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
private fun startForeground() {
|
||||
val notificationId = pushNotificationManager.notificationId
|
||||
val notification = pushNotificationManager.createForegroundNotification()
|
||||
|
||||
if (Build.VERSION.SDK_INT >= 29) {
|
||||
startForeground(notificationId, notification, ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC)
|
||||
} else {
|
||||
startForeground(notificationId, notification)
|
||||
}
|
||||
}
|
||||
|
||||
private fun initializePushController() {
|
||||
// When the app is killed by the system and later recreated to start this service nobody else is initializing
|
||||
// PushController. So we'll have to do it here.
|
||||
pushController.init()
|
||||
}
|
||||
|
||||
override fun onBind(intent: Intent?): IBinder? = null
|
||||
}
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
package com.fsck.k9.controller.push
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
import timber.log.Timber
|
||||
|
||||
/**
|
||||
* Manages starting and stopping [PushService].
|
||||
*/
|
||||
internal class PushServiceManager(private val context: Context) {
|
||||
private var isServiceStarted = AtomicBoolean(false)
|
||||
|
||||
fun start() {
|
||||
Timber.v("PushServiceManager.start()")
|
||||
if (isServiceStarted.compareAndSet(false, true)) {
|
||||
startService()
|
||||
} else {
|
||||
Timber.v("..PushService already running")
|
||||
}
|
||||
}
|
||||
|
||||
fun stop() {
|
||||
Timber.v("PushServiceManager.stop()")
|
||||
if (isServiceStarted.compareAndSet(true, false)) {
|
||||
stopService()
|
||||
} else {
|
||||
Timber.v("..PushService is not running")
|
||||
}
|
||||
}
|
||||
|
||||
private fun startService() {
|
||||
try {
|
||||
val intent = Intent(context, PushService::class.java)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
context.startForegroundService(intent)
|
||||
} else {
|
||||
context.startService(intent)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "Exception while trying to start PushService")
|
||||
}
|
||||
}
|
||||
|
||||
private fun stopService() {
|
||||
try {
|
||||
val intent = Intent(context, PushService::class.java)
|
||||
context.stopService(intent)
|
||||
} catch (e: Exception) {
|
||||
Timber.w(e, "Exception while trying to stop PushService")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
package com.fsck.k9.crypto
|
||||
|
||||
import android.content.ContentValues
|
||||
import com.fsck.k9.mail.Message
|
||||
import com.fsck.k9.message.extractors.PreviewResult
|
||||
|
||||
interface EncryptionExtractor {
|
||||
fun extractEncryption(message: Message): EncryptionResult?
|
||||
}
|
||||
|
||||
data class EncryptionResult(
|
||||
val encryptionType: String,
|
||||
val attachmentCount: Int,
|
||||
val previewResult: PreviewResult = PreviewResult.encrypted(),
|
||||
val textForSearchIndex: String? = null,
|
||||
val extraContentValues: ContentValues? = null
|
||||
)
|
||||
11
app/core/src/main/java/com/fsck/k9/crypto/KoinModule.kt
Normal file
11
app/core/src/main/java/com/fsck/k9/crypto/KoinModule.kt
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
package com.fsck.k9.crypto
|
||||
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import org.koin.dsl.module
|
||||
import org.openintents.openpgp.OpenPgpApiManager
|
||||
|
||||
val openPgpModule = module {
|
||||
factory { (lifecycleOwner: LifecycleOwner) ->
|
||||
OpenPgpApiManager(get(), lifecycleOwner)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,303 @@
|
|||
package com.fsck.k9.crypto;
|
||||
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Stack;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
|
||||
import com.fsck.k9.helper.StringHelper;
|
||||
import com.fsck.k9.mail.Body;
|
||||
import com.fsck.k9.mail.BodyPart;
|
||||
import com.fsck.k9.mail.MessagingException;
|
||||
import com.fsck.k9.mail.Multipart;
|
||||
import com.fsck.k9.mail.Part;
|
||||
import com.fsck.k9.mail.internet.MessageExtractor;
|
||||
import com.fsck.k9.mail.internet.MimeBodyPart;
|
||||
import com.fsck.k9.mail.internet.MimeMultipart;
|
||||
import com.fsck.k9.mail.internet.MimeUtility;
|
||||
import com.fsck.k9.mailstore.CryptoResultAnnotation;
|
||||
import com.fsck.k9.mailstore.MessageCryptoAnnotations;
|
||||
|
||||
import static com.fsck.k9.mail.internet.MimeUtility.isSameMimeType;
|
||||
|
||||
|
||||
public class MessageCryptoStructureDetector {
|
||||
private static final String MULTIPART_ENCRYPTED = "multipart/encrypted";
|
||||
private static final String MULTIPART_SIGNED = "multipart/signed";
|
||||
private static final String PROTOCOL_PARAMETER = "protocol";
|
||||
private static final String APPLICATION_PGP_ENCRYPTED = "application/pgp-encrypted";
|
||||
private static final String APPLICATION_PGP_SIGNATURE = "application/pgp-signature";
|
||||
private static final String TEXT_PLAIN = "text/plain";
|
||||
// APPLICATION/PGP is a special case which occurs from mutt. see http://www.mutt.org/doc/PGP-Notes.txt
|
||||
private static final String APPLICATION_PGP = "application/pgp";
|
||||
|
||||
private static final String PGP_INLINE_START_MARKER = "-----BEGIN PGP MESSAGE-----";
|
||||
private static final String PGP_INLINE_SIGNED_START_MARKER = "-----BEGIN PGP SIGNED MESSAGE-----";
|
||||
private static final int TEXT_LENGTH_FOR_INLINE_CHECK = 36;
|
||||
|
||||
|
||||
public static Part findPrimaryEncryptedOrSignedPart(Part part, List<Part> outputExtraParts) {
|
||||
if (isPartEncryptedOrSigned(part)) {
|
||||
return part;
|
||||
}
|
||||
|
||||
Part foundPart;
|
||||
|
||||
foundPart = findPrimaryPartInAlternative(part);
|
||||
if (foundPart != null) {
|
||||
return foundPart;
|
||||
}
|
||||
|
||||
foundPart = findPrimaryPartInMixed(part, outputExtraParts);
|
||||
if (foundPart != null) {
|
||||
return foundPart;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private static Part findPrimaryPartInMixed(Part part, List<Part> outputExtraParts) {
|
||||
Body body = part.getBody();
|
||||
|
||||
boolean isMultipartMixed = part.isMimeType("multipart/mixed") && body instanceof Multipart;
|
||||
if (!isMultipartMixed) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Multipart multipart = (Multipart) body;
|
||||
if (multipart.getCount() == 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
BodyPart firstBodyPart = multipart.getBodyPart(0);
|
||||
|
||||
Part foundPart;
|
||||
if (isPartEncryptedOrSigned(firstBodyPart)) {
|
||||
foundPart = firstBodyPart;
|
||||
} else {
|
||||
foundPart = findPrimaryPartInAlternative(firstBodyPart);
|
||||
}
|
||||
|
||||
if (foundPart != null && outputExtraParts != null) {
|
||||
for (int i = 1; i < multipart.getCount(); i++) {
|
||||
outputExtraParts.add(multipart.getBodyPart(i));
|
||||
}
|
||||
}
|
||||
|
||||
return foundPart;
|
||||
}
|
||||
|
||||
private static Part findPrimaryPartInAlternative(Part part) {
|
||||
Body body = part.getBody();
|
||||
if (part.isMimeType("multipart/alternative") && body instanceof Multipart) {
|
||||
Multipart multipart = (Multipart) body;
|
||||
if (multipart.getCount() == 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
BodyPart firstBodyPart = multipart.getBodyPart(0);
|
||||
if (isPartPgpInlineEncryptedOrSigned(firstBodyPart)) {
|
||||
return firstBodyPart;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public static List<Part> findMultipartEncryptedParts(Part startPart) {
|
||||
List<Part> encryptedParts = new ArrayList<>();
|
||||
Stack<Part> partsToCheck = new Stack<>();
|
||||
partsToCheck.push(startPart);
|
||||
|
||||
while (!partsToCheck.isEmpty()) {
|
||||
Part part = partsToCheck.pop();
|
||||
Body body = part.getBody();
|
||||
|
||||
if (isPartMultipartEncrypted(part)) {
|
||||
encryptedParts.add(part);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (body instanceof Multipart) {
|
||||
Multipart multipart = (Multipart) body;
|
||||
for (int i = multipart.getCount() - 1; i >= 0; i--) {
|
||||
BodyPart bodyPart = multipart.getBodyPart(i);
|
||||
partsToCheck.push(bodyPart);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return encryptedParts;
|
||||
}
|
||||
|
||||
public static List<Part> findMultipartSignedParts(Part startPart, MessageCryptoAnnotations messageCryptoAnnotations) {
|
||||
List<Part> signedParts = new ArrayList<>();
|
||||
Stack<Part> partsToCheck = new Stack<>();
|
||||
partsToCheck.push(startPart);
|
||||
|
||||
while (!partsToCheck.isEmpty()) {
|
||||
Part part = partsToCheck.pop();
|
||||
if (messageCryptoAnnotations.has(part)) {
|
||||
CryptoResultAnnotation resultAnnotation = messageCryptoAnnotations.get(part);
|
||||
MimeBodyPart replacementData = resultAnnotation.getReplacementData();
|
||||
if (replacementData != null) {
|
||||
part = replacementData;
|
||||
}
|
||||
}
|
||||
Body body = part.getBody();
|
||||
|
||||
if (isPartMultipartSigned(part)) {
|
||||
signedParts.add(part);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (body instanceof Multipart) {
|
||||
Multipart multipart = (Multipart) body;
|
||||
for (int i = multipart.getCount() - 1; i >= 0; i--) {
|
||||
BodyPart bodyPart = multipart.getBodyPart(i);
|
||||
partsToCheck.push(bodyPart);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return signedParts;
|
||||
}
|
||||
|
||||
public static List<Part> findPgpInlineParts(Part startPart) {
|
||||
List<Part> inlineParts = new ArrayList<>();
|
||||
Stack<Part> partsToCheck = new Stack<>();
|
||||
partsToCheck.push(startPart);
|
||||
|
||||
while (!partsToCheck.isEmpty()) {
|
||||
Part part = partsToCheck.pop();
|
||||
Body body = part.getBody();
|
||||
|
||||
if (isPartPgpInlineEncryptedOrSigned(part)) {
|
||||
inlineParts.add(part);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (body instanceof Multipart) {
|
||||
Multipart multipart = (Multipart) body;
|
||||
for (int i = multipart.getCount() - 1; i >= 0; i--) {
|
||||
BodyPart bodyPart = multipart.getBodyPart(i);
|
||||
partsToCheck.push(bodyPart);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return inlineParts;
|
||||
}
|
||||
|
||||
public static byte[] getSignatureData(Part part) throws IOException, MessagingException {
|
||||
if (isPartMultipartSigned(part)) {
|
||||
Body body = part.getBody();
|
||||
if (body instanceof Multipart) {
|
||||
Multipart multi = (Multipart) body;
|
||||
BodyPart signatureBody = multi.getBodyPart(1);
|
||||
ByteArrayOutputStream bos = new ByteArrayOutputStream();
|
||||
signatureBody.getBody().writeTo(bos);
|
||||
return bos.toByteArray();
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static boolean isPartEncryptedOrSigned(Part part) {
|
||||
return isPartMultipartEncrypted(part) || isPartMultipartSigned(part) || isPartPgpInlineEncryptedOrSigned(part);
|
||||
}
|
||||
|
||||
private static boolean isPartMultipartSigned(Part part) {
|
||||
if (!isSameMimeType(part.getMimeType(), MULTIPART_SIGNED)) {
|
||||
return false;
|
||||
}
|
||||
if (! (part.getBody() instanceof MimeMultipart)) {
|
||||
return false;
|
||||
}
|
||||
MimeMultipart mimeMultipart = (MimeMultipart) part.getBody();
|
||||
if (mimeMultipart.getCount() != 2) {
|
||||
return false;
|
||||
}
|
||||
|
||||
String protocolParameter = MimeUtility.getHeaderParameter(part.getContentType(), PROTOCOL_PARAMETER);
|
||||
|
||||
// for partially downloaded messages the protocol parameter isn't yet available, so we'll just assume it's ok
|
||||
boolean dataUnavailable = protocolParameter == null && mimeMultipart.getBodyPart(0).getBody() == null;
|
||||
boolean protocolMatches = isSameMimeType(protocolParameter, mimeMultipart.getBodyPart(1).getMimeType());
|
||||
return dataUnavailable || protocolMatches;
|
||||
}
|
||||
|
||||
public static boolean isPartMultipartEncrypted(Part part) {
|
||||
if (!isSameMimeType(part.getMimeType(), MULTIPART_ENCRYPTED)) {
|
||||
return false;
|
||||
}
|
||||
if (! (part.getBody() instanceof MimeMultipart)) {
|
||||
return false;
|
||||
}
|
||||
MimeMultipart mimeMultipart = (MimeMultipart) part.getBody();
|
||||
if (mimeMultipart.getCount() != 2) {
|
||||
return false;
|
||||
}
|
||||
|
||||
String protocolParameter = MimeUtility.getHeaderParameter(part.getContentType(), PROTOCOL_PARAMETER);
|
||||
|
||||
// for partially downloaded messages the protocol parameter isn't yet available, so we'll just assume it's ok
|
||||
boolean dataUnavailable = protocolParameter == null && mimeMultipart.getBodyPart(1).getBody() == null;
|
||||
boolean protocolMatches = isSameMimeType(protocolParameter, mimeMultipart.getBodyPart(0).getMimeType());
|
||||
return dataUnavailable || protocolMatches;
|
||||
}
|
||||
|
||||
public static boolean isMultipartEncryptedOpenPgpProtocol(Part part) {
|
||||
if (!isSameMimeType(part.getMimeType(), MULTIPART_ENCRYPTED)) {
|
||||
throw new IllegalArgumentException("Part is not multipart/encrypted!");
|
||||
}
|
||||
|
||||
String protocolParameter = MimeUtility.getHeaderParameter(part.getContentType(), PROTOCOL_PARAMETER);
|
||||
return APPLICATION_PGP_ENCRYPTED.equalsIgnoreCase(protocolParameter);
|
||||
}
|
||||
|
||||
public static boolean isMultipartSignedOpenPgpProtocol(Part part) {
|
||||
if (!isSameMimeType(part.getMimeType(), MULTIPART_SIGNED)) {
|
||||
throw new IllegalArgumentException("Part is not multipart/signed!");
|
||||
}
|
||||
|
||||
String protocolParameter = MimeUtility.getHeaderParameter(part.getContentType(), PROTOCOL_PARAMETER);
|
||||
return APPLICATION_PGP_SIGNATURE.equalsIgnoreCase(protocolParameter);
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
static boolean isPartPgpInlineEncryptedOrSigned(Part part) {
|
||||
if (!part.isMimeType(TEXT_PLAIN) && !part.isMimeType(APPLICATION_PGP)) {
|
||||
return false;
|
||||
}
|
||||
String text = MessageExtractor.getTextFromPart(part, TEXT_LENGTH_FOR_INLINE_CHECK);
|
||||
if (StringHelper.isNullOrEmpty(text)) {
|
||||
return false;
|
||||
}
|
||||
text = text.trim();
|
||||
return text.startsWith(PGP_INLINE_START_MARKER) || text.startsWith(PGP_INLINE_SIGNED_START_MARKER);
|
||||
}
|
||||
|
||||
public static boolean isPartPgpInlineEncrypted(@Nullable Part part) {
|
||||
if (part == null) {
|
||||
return false;
|
||||
}
|
||||
if (!part.isMimeType(TEXT_PLAIN) && !part.isMimeType(APPLICATION_PGP)) {
|
||||
return false;
|
||||
}
|
||||
String text = MessageExtractor.getTextFromPart(part, TEXT_LENGTH_FOR_INLINE_CHECK);
|
||||
if (StringHelper.isNullOrEmpty(text)) {
|
||||
return false;
|
||||
}
|
||||
text = text.trim();
|
||||
return text.startsWith(PGP_INLINE_START_MARKER);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
package com.fsck.k9.crypto;
|
||||
|
||||
|
||||
import com.fsck.k9.Identity;
|
||||
import com.fsck.k9.helper.StringHelper;
|
||||
|
||||
|
||||
public class OpenPgpApiHelper {
|
||||
|
||||
/**
|
||||
* Create an "account name" from the supplied identity for use with the OpenPgp API's
|
||||
* <code>EXTRA_ACCOUNT_NAME</code>.
|
||||
*
|
||||
* @return A string with the following format:
|
||||
* <code>display name <user@example.com></code>
|
||||
*/
|
||||
public static String buildUserId(Identity identity) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
|
||||
String name = identity.getName();
|
||||
if (!StringHelper.isNullOrEmpty(name)) {
|
||||
sb.append(name).append(" ");
|
||||
}
|
||||
sb.append("<").append(identity.getEmail()).append(">");
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
package com.fsck.k9.helper
|
||||
|
||||
import android.app.AlarmManager
|
||||
import android.app.PendingIntent
|
||||
import android.os.Build
|
||||
|
||||
class AlarmManagerCompat(private val alarmManager: AlarmManager) {
|
||||
fun scheduleAlarm(triggerAtMillis: Long, operation: PendingIntent) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
alarmManager.setExactAndAllowWhileIdle(AlarmManager.ELAPSED_REALTIME_WAKEUP, triggerAtMillis, operation)
|
||||
} else {
|
||||
alarmManager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, triggerAtMillis, operation)
|
||||
}
|
||||
}
|
||||
|
||||
fun cancelAlarm(operation: PendingIntent) {
|
||||
alarmManager.cancel(operation)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
package com.fsck.k9.helper
|
||||
|
||||
import android.content.Context
|
||||
import com.fsck.k9.mail.ssl.KeyStoreDirectoryProvider
|
||||
import java.io.File
|
||||
|
||||
internal class AndroidKeyStoreDirectoryProvider(private val context: Context) : KeyStoreDirectoryProvider {
|
||||
override fun getDirectory(): File {
|
||||
return context.getDir("KeyStore", Context.MODE_PRIVATE)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
package com.fsck.k9.helper
|
||||
|
||||
import android.content.ClipData
|
||||
import android.content.Context
|
||||
|
||||
/**
|
||||
* Access the system clipboard
|
||||
*/
|
||||
class ClipboardManager(private val context: Context) {
|
||||
|
||||
/**
|
||||
* Copy a text string to the system clipboard
|
||||
*
|
||||
* @param label User-visible label for the content.
|
||||
* @param text The actual text to be copied to the clipboard.
|
||||
*/
|
||||
fun setText(label: String, text: String) {
|
||||
val clipboardManager = context.getSystemService(Context.CLIPBOARD_SERVICE) as android.content.ClipboardManager
|
||||
val clip = ClipData.newPlainText(label, text)
|
||||
clipboardManager.setPrimaryClip(clip)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
package com.fsck.k9.helper
|
||||
|
||||
/**
|
||||
* Returns a [Set] containing the results of applying the given [transform] function to each element in the original
|
||||
* collection.
|
||||
*
|
||||
* If you know the size of the output or can make an educated guess, specify [expectedSize] as an optimization.
|
||||
* The initial capacity of the `Set` will be derived from this value.
|
||||
*/
|
||||
inline fun <T, R> Iterable<T>.mapToSet(expectedSize: Int? = null, transform: (T) -> R): Set<R> {
|
||||
return if (expectedSize != null) {
|
||||
mapTo(LinkedHashSet(setCapacity(expectedSize)), transform)
|
||||
} else {
|
||||
mapTo(mutableSetOf(), transform)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a [Set] containing the results of applying the given [transform] function to each element in the original
|
||||
* collection.
|
||||
*
|
||||
* The size of the output is expected to be equal to the size of the input. If that's not the case, please use
|
||||
* [mapToSet] instead.
|
||||
*/
|
||||
inline fun <T, R> Collection<T>.mapCollectionToSet(transform: (T) -> R): Set<R> {
|
||||
return mapToSet(expectedSize = size, transform)
|
||||
}
|
||||
|
||||
// A copy of Kotlin's internal mapCapacity() for the JVM
|
||||
fun setCapacity(expectedSize: Int): Int = when {
|
||||
// We are not coercing the value to a valid one and not throwing an exception. It is up to the caller to
|
||||
// properly handle negative values.
|
||||
expectedSize < 0 -> expectedSize
|
||||
expectedSize < 3 -> expectedSize + 1
|
||||
expectedSize < INT_MAX_POWER_OF_TWO -> ((expectedSize / 0.75F) + 1.0F).toInt()
|
||||
// any large value
|
||||
else -> Int.MAX_VALUE
|
||||
}
|
||||
|
||||
private const val INT_MAX_POWER_OF_TWO: Int = 1 shl (Int.SIZE_BITS - 2)
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
package com.fsck.k9.helper
|
||||
|
||||
import app.k9mail.core.android.common.contact.ContactRepository
|
||||
import app.k9mail.core.common.mail.EmailAddress
|
||||
|
||||
interface ContactNameProvider {
|
||||
fun getNameForAddress(address: String): String?
|
||||
}
|
||||
|
||||
class RealContactNameProvider(
|
||||
private val contactRepository: ContactRepository,
|
||||
) : ContactNameProvider {
|
||||
override fun getNameForAddress(address: String): String? {
|
||||
return contactRepository.getContactFor(EmailAddress(address))?.name
|
||||
}
|
||||
}
|
||||
16
app/core/src/main/java/com/fsck/k9/helper/Contacts.kt
Normal file
16
app/core/src/main/java/com/fsck/k9/helper/Contacts.kt
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
package com.fsck.k9.helper
|
||||
|
||||
import com.fsck.k9.mail.Address
|
||||
|
||||
/**
|
||||
* Helper class to access the contacts stored on the device.
|
||||
*/
|
||||
class Contacts {
|
||||
/**
|
||||
* Mark contacts with the provided email addresses as contacted.
|
||||
*/
|
||||
fun markAsContacted(addresses: Array<Address?>?) {
|
||||
// TODO: Keep track of this information in a local database. Then use this information when sorting contacts for
|
||||
// auto-completion.
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
@file:JvmName("CrLfConverter")
|
||||
|
||||
package com.fsck.k9.helper
|
||||
|
||||
fun String?.toLf() = this?.replace("\r\n", "\n")
|
||||
|
||||
fun CharSequence?.toLf() = this?.toString()?.replace("\r\n", "\n")
|
||||
|
||||
fun CharSequence?.toCrLf() = this?.toString()?.replace("\n", "\r\n")
|
||||
|
|
@ -0,0 +1,167 @@
|
|||
package com.fsck.k9.helper;
|
||||
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.Socket;
|
||||
import java.security.KeyManagementException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import android.content.Context;
|
||||
import android.net.SSLCertificateSocketFactory;
|
||||
import android.os.Build;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import com.fsck.k9.mail.MessagingException;
|
||||
import com.fsck.k9.mail.ssl.TrustManagerFactory;
|
||||
import com.fsck.k9.mail.ssl.TrustedSocketFactory;
|
||||
import javax.net.ssl.KeyManager;
|
||||
import javax.net.ssl.SNIHostName;
|
||||
import javax.net.ssl.SNIServerName;
|
||||
import javax.net.ssl.SSLContext;
|
||||
import javax.net.ssl.SSLParameters;
|
||||
import javax.net.ssl.SSLSocket;
|
||||
import javax.net.ssl.SSLSocketFactory;
|
||||
import javax.net.ssl.TrustManager;
|
||||
import timber.log.Timber;
|
||||
|
||||
|
||||
public class DefaultTrustedSocketFactory implements TrustedSocketFactory {
|
||||
private static final String[] ENABLED_CIPHERS;
|
||||
private static final String[] ENABLED_PROTOCOLS;
|
||||
|
||||
private static final String[] DISALLOWED_CIPHERS = {
|
||||
"SSL_RSA_WITH_DES_CBC_SHA",
|
||||
"SSL_DHE_RSA_WITH_DES_CBC_SHA",
|
||||
"SSL_DHE_DSS_WITH_DES_CBC_SHA",
|
||||
"SSL_RSA_EXPORT_WITH_RC4_40_MD5",
|
||||
"SSL_RSA_EXPORT_WITH_DES40_CBC_SHA",
|
||||
"SSL_DHE_RSA_EXPORT_WITH_DES40_CBC_SHA",
|
||||
"SSL_DHE_DSS_EXPORT_WITH_DES40_CBC_SHA",
|
||||
"SSL_RSA_WITH_3DES_EDE_CBC_SHA",
|
||||
"SSL_DHE_RSA_WITH_3DES_EDE_CBC_SHA",
|
||||
"SSL_DHE_DSS_WITH_3DES_EDE_CBC_SHA",
|
||||
"TLS_ECDHE_RSA_WITH_RC4_128_SHA",
|
||||
"TLS_ECDHE_ECDSA_WITH_RC4_128_SHA",
|
||||
"TLS_ECDH_RSA_WITH_RC4_128_SHA",
|
||||
"TLS_ECDH_ECDSA_WITH_RC4_128_SHA",
|
||||
"SSL_RSA_WITH_RC4_128_SHA",
|
||||
"SSL_RSA_WITH_RC4_128_MD5",
|
||||
"TLS_ECDH_RSA_WITH_NULL_SHA",
|
||||
"TLS_ECDH_anon_WITH_3DES_EDE_CBC_SHA",
|
||||
"TLS_ECDH_anon_WITH_NULL_SHA",
|
||||
"TLS_ECDH_anon_WITH_RC4_128_SHA",
|
||||
"TLS_RSA_WITH_NULL_SHA256"
|
||||
};
|
||||
|
||||
private static final String[] DISALLOWED_PROTOCOLS = {
|
||||
"SSLv3"
|
||||
};
|
||||
|
||||
static {
|
||||
String[] enabledCiphers = null;
|
||||
String[] supportedProtocols = null;
|
||||
|
||||
try {
|
||||
SSLContext sslContext = SSLContext.getInstance("TLS");
|
||||
sslContext.init(null, null, null);
|
||||
SSLSocketFactory sf = sslContext.getSocketFactory();
|
||||
SSLSocket sock = (SSLSocket) sf.createSocket();
|
||||
enabledCiphers = sock.getEnabledCipherSuites();
|
||||
|
||||
/*
|
||||
* Retrieve all supported protocols, not just the (default) enabled
|
||||
* ones. TLSv1.1 & TLSv1.2 are supported on API levels 16+, but are
|
||||
* only enabled by default on API levels 20+.
|
||||
*/
|
||||
supportedProtocols = sock.getSupportedProtocols();
|
||||
} catch (Exception e) {
|
||||
Timber.e(e, "Error getting information about available SSL/TLS ciphers and protocols");
|
||||
}
|
||||
|
||||
ENABLED_CIPHERS = (enabledCiphers == null) ? null : remove(enabledCiphers, DISALLOWED_CIPHERS);
|
||||
ENABLED_PROTOCOLS = (supportedProtocols == null) ? null : remove(supportedProtocols, DISALLOWED_PROTOCOLS);
|
||||
}
|
||||
|
||||
private final Context context;
|
||||
private final TrustManagerFactory trustManagerFactory;
|
||||
|
||||
public DefaultTrustedSocketFactory(Context context, TrustManagerFactory trustManagerFactory) {
|
||||
this.context = context;
|
||||
this.trustManagerFactory = trustManagerFactory;
|
||||
}
|
||||
|
||||
protected static String[] remove(String[] enabled, String[] disallowed) {
|
||||
List<String> items = new ArrayList<>();
|
||||
Collections.addAll(items, enabled);
|
||||
|
||||
if (disallowed != null) {
|
||||
for (String item : disallowed) {
|
||||
items.remove(item);
|
||||
}
|
||||
}
|
||||
|
||||
return items.toArray(new String[0]);
|
||||
}
|
||||
|
||||
public Socket createSocket(Socket socket, String host, int port, String clientCertificateAlias)
|
||||
throws NoSuchAlgorithmException, KeyManagementException, MessagingException, IOException {
|
||||
|
||||
TrustManager[] trustManagers = new TrustManager[] { trustManagerFactory.getTrustManagerForDomain(host, port) };
|
||||
KeyManager[] keyManagers = null;
|
||||
if (!TextUtils.isEmpty(clientCertificateAlias)) {
|
||||
keyManagers = new KeyManager[] { new KeyChainKeyManager(context, clientCertificateAlias) };
|
||||
}
|
||||
|
||||
SSLContext sslContext = SSLContext.getInstance("TLS");
|
||||
sslContext.init(keyManagers, trustManagers, null);
|
||||
SSLSocketFactory socketFactory = sslContext.getSocketFactory();
|
||||
Socket trustedSocket;
|
||||
if (socket == null) {
|
||||
trustedSocket = socketFactory.createSocket();
|
||||
} else {
|
||||
trustedSocket = socketFactory.createSocket(socket, host, port, true);
|
||||
}
|
||||
|
||||
SSLSocket sslSocket = (SSLSocket) trustedSocket;
|
||||
|
||||
hardenSocket(sslSocket);
|
||||
|
||||
setSniHost(socketFactory, sslSocket, host);
|
||||
|
||||
return trustedSocket;
|
||||
}
|
||||
|
||||
private static void hardenSocket(SSLSocket sock) {
|
||||
if (ENABLED_CIPHERS != null) {
|
||||
sock.setEnabledCipherSuites(ENABLED_CIPHERS);
|
||||
}
|
||||
if (ENABLED_PROTOCOLS != null) {
|
||||
sock.setEnabledProtocols(ENABLED_PROTOCOLS);
|
||||
}
|
||||
}
|
||||
|
||||
public static void setSniHost(SSLSocketFactory factory, SSLSocket socket, String hostname) {
|
||||
if (factory instanceof android.net.SSLCertificateSocketFactory) {
|
||||
SSLCertificateSocketFactory sslCertificateSocketFactory = (SSLCertificateSocketFactory) factory;
|
||||
sslCertificateSocketFactory.setHostname(socket, hostname);
|
||||
} else if (Build.VERSION.SDK_INT >= 24) {
|
||||
SSLParameters sslParameters = socket.getSSLParameters();
|
||||
List<SNIServerName> sniServerNames = Collections.singletonList(new SNIHostName(hostname));
|
||||
sslParameters.setServerNames(sniServerNames);
|
||||
socket.setSSLParameters(sslParameters);
|
||||
} else {
|
||||
setHostnameViaReflection(socket, hostname);
|
||||
}
|
||||
}
|
||||
|
||||
private static void setHostnameViaReflection(SSLSocket socket, String hostname) {
|
||||
try {
|
||||
socket.getClass().getMethod("setHostname", String.class).invoke(socket, hostname);
|
||||
} catch (Throwable e) {
|
||||
Timber.e(e, "Could not call SSLSocket#setHostname(String) method ");
|
||||
}
|
||||
}
|
||||
}
|
||||
147
app/core/src/main/java/com/fsck/k9/helper/FileHelper.java
Normal file
147
app/core/src/main/java/com/fsck/k9/helper/FileHelper.java
Normal file
|
|
@ -0,0 +1,147 @@
|
|||
package com.fsck.k9.helper;
|
||||
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
|
||||
import timber.log.Timber;
|
||||
|
||||
import org.apache.commons.io.IOUtils;
|
||||
|
||||
|
||||
public class FileHelper {
|
||||
public static void touchFile(final File parentDir, final String name) {
|
||||
final File file = new File(parentDir, name);
|
||||
try {
|
||||
if (!file.exists()) {
|
||||
if (!file.createNewFile()) {
|
||||
Timber.d("Unable to create file: %s", file.getAbsolutePath());
|
||||
}
|
||||
} else {
|
||||
if (!file.setLastModified(System.currentTimeMillis())) {
|
||||
Timber.d("Unable to change last modification date: %s", file.getAbsolutePath());
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Timber.d(e, "Unable to touch file: %s", file.getAbsolutePath());
|
||||
}
|
||||
}
|
||||
|
||||
private static void copyFile(File from, File to) throws IOException {
|
||||
FileInputStream in = new FileInputStream(from);
|
||||
FileOutputStream out = new FileOutputStream(to);
|
||||
try {
|
||||
byte[] buffer = new byte[1024];
|
||||
int count;
|
||||
while ((count = in.read(buffer)) > 0) {
|
||||
out.write(buffer, 0, count);
|
||||
}
|
||||
out.close();
|
||||
} finally {
|
||||
IOUtils.closeQuietly(in);
|
||||
IOUtils.closeQuietly(out);
|
||||
}
|
||||
}
|
||||
|
||||
public static void renameOrMoveByCopying(File from, File to) throws IOException {
|
||||
deleteFileIfExists(to);
|
||||
|
||||
boolean renameFailed = !from.renameTo(to);
|
||||
if (renameFailed) {
|
||||
copyFile(from, to);
|
||||
|
||||
boolean deleteFromFailed = !from.delete();
|
||||
if (deleteFromFailed) {
|
||||
Timber.e("Unable to delete source file after copying to destination!");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void deleteFileIfExists(File to) throws IOException {
|
||||
boolean fileDoesNotExist = !to.exists();
|
||||
if (fileDoesNotExist) {
|
||||
return;
|
||||
}
|
||||
|
||||
boolean deleteOk = to.delete();
|
||||
if (deleteOk) {
|
||||
return;
|
||||
}
|
||||
|
||||
throw new IOException("Unable to delete file: " + to.getAbsolutePath());
|
||||
}
|
||||
|
||||
public static boolean move(final File from, final File to) {
|
||||
if (to.exists()) {
|
||||
if (!to.delete()) {
|
||||
Timber.d("Unable to delete file: %s", to.getAbsolutePath());
|
||||
}
|
||||
}
|
||||
|
||||
if (!to.getParentFile().mkdirs()) {
|
||||
Timber.d("Unable to make directories: %s", to.getParentFile().getAbsolutePath());
|
||||
}
|
||||
|
||||
try {
|
||||
copyFile(from, to);
|
||||
|
||||
boolean deleteFromFailed = !from.delete();
|
||||
if (deleteFromFailed) {
|
||||
Timber.e("Unable to delete source file after copying to destination!");
|
||||
}
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
Timber.w(e, "cannot move %s to %s", from.getAbsolutePath(), to.getAbsolutePath());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public static void moveRecursive(final File fromDir, final File toDir) {
|
||||
if (!fromDir.exists()) {
|
||||
return;
|
||||
}
|
||||
if (!fromDir.isDirectory()) {
|
||||
if (toDir.exists()) {
|
||||
if (!toDir.delete()) {
|
||||
Timber.w("cannot delete already existing file/directory %s", toDir.getAbsolutePath());
|
||||
}
|
||||
}
|
||||
if (!fromDir.renameTo(toDir)) {
|
||||
Timber.w("cannot rename %s to %s - moving instead", fromDir.getAbsolutePath(), toDir.getAbsolutePath());
|
||||
move(fromDir, toDir);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (!toDir.exists() || !toDir.isDirectory()) {
|
||||
if (toDir.exists()) {
|
||||
if (!toDir.delete()) {
|
||||
Timber.d("Unable to delete file: %s", toDir.getAbsolutePath());
|
||||
}
|
||||
}
|
||||
if (!toDir.mkdirs()) {
|
||||
Timber.w("cannot create directory %s", toDir.getAbsolutePath());
|
||||
}
|
||||
}
|
||||
File[] files = fromDir.listFiles();
|
||||
for (File file : files) {
|
||||
if (file.isDirectory()) {
|
||||
moveRecursive(file, new File(toDir, file.getName()));
|
||||
if (!file.delete()) {
|
||||
Timber.d("Unable to delete file: %s", toDir.getAbsolutePath());
|
||||
}
|
||||
} else {
|
||||
File target = new File(toDir, file.getName());
|
||||
if (!file.renameTo(target)) {
|
||||
Timber.w("cannot rename %s to %s - moving instead",
|
||||
file.getAbsolutePath(), target.getAbsolutePath());
|
||||
move(file, target);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!fromDir.delete()) {
|
||||
Timber.w("cannot delete %s", fromDir.getAbsolutePath());
|
||||
}
|
||||
}
|
||||
}
|
||||
40
app/core/src/main/java/com/fsck/k9/helper/IdentityHelper.kt
Normal file
40
app/core/src/main/java/com/fsck/k9/helper/IdentityHelper.kt
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
package com.fsck.k9.helper
|
||||
|
||||
import com.fsck.k9.Account
|
||||
import com.fsck.k9.Identity
|
||||
import com.fsck.k9.mail.Message
|
||||
import com.fsck.k9.mail.Message.RecipientType
|
||||
|
||||
object IdentityHelper {
|
||||
private val RECIPIENT_TYPES = listOf(
|
||||
RecipientType.TO,
|
||||
RecipientType.CC,
|
||||
RecipientType.X_ORIGINAL_TO,
|
||||
RecipientType.DELIVERED_TO,
|
||||
RecipientType.X_ENVELOPE_TO
|
||||
)
|
||||
|
||||
/**
|
||||
* Find the identity a message was sent to.
|
||||
*
|
||||
* @param account
|
||||
* The account the message belongs to.
|
||||
* @param message
|
||||
* The message to get the recipients from.
|
||||
*
|
||||
* @return The identity the message was sent to, or the account's default identity if it
|
||||
* couldn't be determined which identity this message was sent to.
|
||||
*
|
||||
* @see Account.findIdentity
|
||||
*/
|
||||
@JvmStatic
|
||||
fun getRecipientIdentityFromMessage(account: Account, message: Message): Identity {
|
||||
val recipient: Identity? = RECIPIENT_TYPES.asSequence()
|
||||
.flatMap { recipientType -> message.getRecipients(recipientType).asSequence() }
|
||||
.map { address -> account.findIdentity(address) }
|
||||
.filterNotNull()
|
||||
.firstOrNull()
|
||||
|
||||
return recipient ?: account.getIdentity(0)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,190 @@
|
|||
|
||||
package com.fsck.k9.helper;
|
||||
|
||||
import java.net.Socket;
|
||||
import java.security.Principal;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.cert.CertificateException;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
import javax.net.ssl.SSLEngine;
|
||||
import javax.net.ssl.X509ExtendedKeyManager;
|
||||
import javax.security.auth.x500.X500Principal;
|
||||
|
||||
import android.content.Context;
|
||||
import android.security.KeyChain;
|
||||
import android.security.KeyChainException;
|
||||
|
||||
import com.fsck.k9.mail.CertificateValidationException;
|
||||
import com.fsck.k9.mail.MessagingException;
|
||||
import timber.log.Timber;
|
||||
|
||||
import static com.fsck.k9.mail.CertificateValidationException.Reason;
|
||||
import static com.fsck.k9.mail.CertificateValidationException.Reason.RetrievalFailure;
|
||||
|
||||
/**
|
||||
* For client certificate authentication! Provide private keys and certificates
|
||||
* during the TLS handshake using the Android 4.0 KeyChain API.
|
||||
*/
|
||||
class KeyChainKeyManager extends X509ExtendedKeyManager {
|
||||
private final String mAlias;
|
||||
private final X509Certificate[] mChain;
|
||||
private final PrivateKey mPrivateKey;
|
||||
|
||||
|
||||
/**
|
||||
* @param alias Must not be null nor empty
|
||||
* @throws MessagingException
|
||||
* Indicates an error in retrieving the certificate for the alias
|
||||
* (likely because the alias is invalid or the certificate was deleted)
|
||||
*/
|
||||
public KeyChainKeyManager(Context context, String alias) throws MessagingException {
|
||||
mAlias = alias;
|
||||
|
||||
try {
|
||||
mChain = fetchCertificateChain(context, alias);
|
||||
mPrivateKey = fetchPrivateKey(context, alias);
|
||||
} catch (KeyChainException e) {
|
||||
// The certificate was possibly deleted. Notify user of error.
|
||||
throw new CertificateValidationException(e.getMessage(), RetrievalFailure, alias);
|
||||
} catch (InterruptedException e) {
|
||||
throw new CertificateValidationException(e.getMessage(), RetrievalFailure, alias);
|
||||
}
|
||||
}
|
||||
|
||||
private X509Certificate[] fetchCertificateChain(Context context, String alias)
|
||||
throws KeyChainException, InterruptedException, MessagingException {
|
||||
|
||||
X509Certificate[] chain = KeyChain.getCertificateChain(context, alias);
|
||||
if (chain == null || chain.length == 0) {
|
||||
throw new MessagingException("No certificate chain found for: " + alias);
|
||||
}
|
||||
try {
|
||||
for (X509Certificate certificate : chain) {
|
||||
certificate.checkValidity();
|
||||
}
|
||||
} catch (CertificateException e) {
|
||||
throw new CertificateValidationException(e.getMessage(), Reason.Expired, alias);
|
||||
}
|
||||
|
||||
return chain;
|
||||
}
|
||||
|
||||
private PrivateKey fetchPrivateKey(Context context, String alias) throws KeyChainException,
|
||||
InterruptedException, MessagingException {
|
||||
|
||||
PrivateKey privateKey = KeyChain.getPrivateKey(context, alias);
|
||||
if (privateKey == null) {
|
||||
throw new MessagingException("No private key found for: " + alias);
|
||||
}
|
||||
|
||||
return privateKey;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String chooseClientAlias(String[] keyTypes, Principal[] issuers, Socket socket) {
|
||||
return chooseAlias(keyTypes, issuers);
|
||||
}
|
||||
|
||||
@Override
|
||||
public X509Certificate[] getCertificateChain(String alias) {
|
||||
return (mAlias.equals(alias) ? mChain : null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PrivateKey getPrivateKey(String alias) {
|
||||
return (mAlias.equals(alias) ? mPrivateKey : null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String chooseServerAlias(String keyType, Principal[] issuers, Socket socket) {
|
||||
return chooseAlias(new String[] { keyType }, issuers);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String[] getClientAliases(String keyType, Principal[] issuers) {
|
||||
final String al = chooseAlias(new String[] { keyType }, issuers);
|
||||
return (al == null ? null : new String[] { al });
|
||||
}
|
||||
|
||||
@Override
|
||||
public String[] getServerAliases(String keyType, Principal[] issuers) {
|
||||
final String al = chooseAlias(new String[] { keyType }, issuers);
|
||||
return (al == null ? null : new String[] { al });
|
||||
}
|
||||
|
||||
@Override
|
||||
public String chooseEngineClientAlias(String[] keyTypes, Principal[] issuers, SSLEngine engine) {
|
||||
return chooseAlias(keyTypes, issuers);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String chooseEngineServerAlias(String keyType, Principal[] issuers, SSLEngine engine) {
|
||||
return chooseAlias(new String[] { keyType }, issuers);
|
||||
}
|
||||
|
||||
private String chooseAlias(String[] keyTypes, Principal[] issuers) {
|
||||
if (keyTypes == null || keyTypes.length == 0) {
|
||||
return null;
|
||||
}
|
||||
final X509Certificate cert = mChain[0];
|
||||
final String certKeyAlg = cert.getPublicKey().getAlgorithm();
|
||||
final String certSigAlg = cert.getSigAlgName().toUpperCase(Locale.US);
|
||||
for (String keyAlgorithm : keyTypes) {
|
||||
if (keyAlgorithm == null) {
|
||||
continue;
|
||||
}
|
||||
final String sigAlgorithm;
|
||||
// handle cases like EC_EC and EC_RSA
|
||||
int index = keyAlgorithm.indexOf('_');
|
||||
if (index == -1) {
|
||||
sigAlgorithm = null;
|
||||
} else {
|
||||
sigAlgorithm = keyAlgorithm.substring(index + 1);
|
||||
keyAlgorithm = keyAlgorithm.substring(0, index);
|
||||
}
|
||||
// key algorithm does not match
|
||||
if (!certKeyAlg.equals(keyAlgorithm)) {
|
||||
continue;
|
||||
}
|
||||
/*
|
||||
* TODO find a more reliable test for signature
|
||||
* algorithm. Unfortunately value varies with
|
||||
* provider. For example for "EC" it could be
|
||||
* "SHA1WithECDSA" or simply "ECDSA".
|
||||
*/
|
||||
// sig algorithm does not match
|
||||
if (sigAlgorithm != null && certSigAlg != null
|
||||
&& !certSigAlg.contains(sigAlgorithm)) {
|
||||
continue;
|
||||
}
|
||||
// no issuers to match
|
||||
if (issuers == null || issuers.length == 0) {
|
||||
return mAlias;
|
||||
}
|
||||
List<Principal> issuersList = Arrays.asList(issuers);
|
||||
// check that a certificate in the chain was issued by one of the specified issuers
|
||||
for (X509Certificate certFromChain : mChain) {
|
||||
/*
|
||||
* Note use of X500Principal from
|
||||
* getIssuerX500Principal as opposed to Principal
|
||||
* from getIssuerDN. Principal.equals test does
|
||||
* not work in the case where
|
||||
* xcertFromChain.getIssuerDN is a bouncycastle
|
||||
* org.bouncycastle.jce.X509Principal.
|
||||
*/
|
||||
X500Principal issuerFromChain = certFromChain.getIssuerX500Principal();
|
||||
if (issuersList.contains(issuerFromChain)) {
|
||||
return mAlias;
|
||||
}
|
||||
}
|
||||
Timber.w("Client certificate %s not issued by any of the requested issuers", mAlias);
|
||||
return null;
|
||||
}
|
||||
Timber.w("Client certificate %s does not match any of the requested key types", mAlias);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
15
app/core/src/main/java/com/fsck/k9/helper/KoinModule.kt
Normal file
15
app/core/src/main/java/com/fsck/k9/helper/KoinModule.kt
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
package com.fsck.k9.helper
|
||||
|
||||
import android.app.AlarmManager
|
||||
import android.content.Context
|
||||
import com.fsck.k9.mail.ssl.KeyStoreDirectoryProvider
|
||||
import org.koin.dsl.module
|
||||
|
||||
val helperModule = module {
|
||||
single { ClipboardManager(get()) }
|
||||
single { MessageHelper(resourceProvider = get(), contactRepository = get()) }
|
||||
factory<KeyStoreDirectoryProvider> { AndroidKeyStoreDirectoryProvider(context = get()) }
|
||||
factory { get<Context>().getSystemService(Context.ALARM_SERVICE) as AlarmManager }
|
||||
single { AlarmManagerCompat(alarmManager = get()) }
|
||||
factory<ContactNameProvider> { RealContactNameProvider(contactRepository = get()) }
|
||||
}
|
||||
70
app/core/src/main/java/com/fsck/k9/helper/ListHeaders.java
Normal file
70
app/core/src/main/java/com/fsck/k9/helper/ListHeaders.java
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
package com.fsck.k9.helper;
|
||||
|
||||
|
||||
import android.net.Uri;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import com.fsck.k9.mail.Address;
|
||||
import com.fsck.k9.mail.Message;
|
||||
|
||||
|
||||
/**
|
||||
* Intended to cover:
|
||||
*
|
||||
* RFC 2369
|
||||
* The Use of URLs as Meta-Syntax for Core Mail List Commands
|
||||
* and their Transport through Message Header Fields
|
||||
* https://www.ietf.org/rfc/rfc2369.txt
|
||||
*
|
||||
* This is the following fields:
|
||||
*
|
||||
* List-Help
|
||||
* List-Subscribe
|
||||
* List-Unsubscribe
|
||||
* List-Post
|
||||
* List-Owner
|
||||
* List-Archive
|
||||
*
|
||||
* Currently only provides a utility method for List-Post
|
||||
**/
|
||||
public class ListHeaders {
|
||||
public static final String LIST_POST_HEADER = "List-Post";
|
||||
private static final Pattern MAILTO_CONTAINER_PATTERN = Pattern.compile("<(mailto:.+)>");
|
||||
|
||||
|
||||
public static Address[] getListPostAddresses(Message message) {
|
||||
String[] headerValues = message.getHeader(LIST_POST_HEADER);
|
||||
if (headerValues.length < 1) {
|
||||
return new Address[0];
|
||||
}
|
||||
|
||||
List<Address> listPostAddresses = new ArrayList<>();
|
||||
for (String headerValue : headerValues) {
|
||||
Address address = extractAddress(headerValue);
|
||||
if (address != null) {
|
||||
listPostAddresses.add(address);
|
||||
}
|
||||
}
|
||||
|
||||
return listPostAddresses.toArray(new Address[listPostAddresses.size()]);
|
||||
}
|
||||
|
||||
private static Address extractAddress(String headerValue) {
|
||||
if (headerValue == null || headerValue.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Matcher matcher = MAILTO_CONTAINER_PATTERN.matcher(headerValue);
|
||||
if (!matcher.find()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Uri mailToUri = Uri.parse(matcher.group(1));
|
||||
Address[] emailAddress = MailTo.parse(mailToUri).getTo();
|
||||
return emailAddress.length >= 1 ? emailAddress[0] : null;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
package com.fsck.k9.helper
|
||||
|
||||
import android.net.Uri
|
||||
import com.fsck.k9.mail.Message
|
||||
import java.util.regex.Pattern
|
||||
|
||||
object ListUnsubscribeHelper {
|
||||
private const val LIST_UNSUBSCRIBE_HEADER = "List-Unsubscribe"
|
||||
private val MAILTO_CONTAINER_PATTERN = Pattern.compile("<(mailto:.+?)>")
|
||||
private val HTTPS_CONTAINER_PATTERN = Pattern.compile("<(https:.+?)>")
|
||||
|
||||
// As K-9 Mail is an email client, we prefer a mailto: unsubscribe method
|
||||
// but if none is found, a https URL is acceptable too
|
||||
fun getPreferredListUnsubscribeUri(message: Message): UnsubscribeUri? {
|
||||
val headerValues = message.getHeader(LIST_UNSUBSCRIBE_HEADER)
|
||||
if (headerValues.isEmpty()) {
|
||||
return null
|
||||
}
|
||||
val listUnsubscribeUris = mutableListOf<Uri>()
|
||||
for (headerValue in headerValues) {
|
||||
val uri = extractUri(headerValue) ?: continue
|
||||
|
||||
if (uri.scheme == "mailto") {
|
||||
return MailtoUnsubscribeUri(uri)
|
||||
}
|
||||
|
||||
// If we got here it must be HTTPS
|
||||
listUnsubscribeUris.add(uri)
|
||||
}
|
||||
|
||||
if (listUnsubscribeUris.isNotEmpty()) {
|
||||
return HttpsUnsubscribeUri(listUnsubscribeUris[0])
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
private fun extractUri(headerValue: String?): Uri? {
|
||||
if (headerValue == null || headerValue.isEmpty()) {
|
||||
return null
|
||||
}
|
||||
|
||||
var matcher = MAILTO_CONTAINER_PATTERN.matcher(headerValue)
|
||||
if (matcher.find()) {
|
||||
return Uri.parse(matcher.group(1))
|
||||
}
|
||||
|
||||
matcher = HTTPS_CONTAINER_PATTERN.matcher(headerValue)
|
||||
if (matcher.find()) {
|
||||
return Uri.parse(matcher.group(1))
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
}
|
||||
165
app/core/src/main/java/com/fsck/k9/helper/MailTo.java
Normal file
165
app/core/src/main/java/com/fsck/k9/helper/MailTo.java
Normal file
|
|
@ -0,0 +1,165 @@
|
|||
package com.fsck.k9.helper;
|
||||
|
||||
|
||||
import android.net.Uri;
|
||||
|
||||
import com.fsck.k9.mail.Address;
|
||||
import com.fsck.k9.mail.internet.MessageIdParser;
|
||||
import com.fsck.k9.mail.internet.MimeHeaderParserException;
|
||||
import timber.log.Timber;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
|
||||
public final class MailTo {
|
||||
private static final String MAILTO_SCHEME = "mailto";
|
||||
private static final String TO = "to";
|
||||
private static final String IN_REPLY_TO = "in-reply-to";
|
||||
private static final String BODY = "body";
|
||||
private static final String CC = "cc";
|
||||
private static final String BCC = "bcc";
|
||||
private static final String SUBJECT = "subject";
|
||||
|
||||
private static final Address[] EMPTY_ADDRESS_LIST = new Address[0];
|
||||
|
||||
|
||||
private final Address[] toAddresses;
|
||||
private final Address[] ccAddresses;
|
||||
private final Address[] bccAddresses;
|
||||
private final String inReplyToMessageId;
|
||||
private final String subject;
|
||||
private final String body;
|
||||
|
||||
|
||||
public static boolean isMailTo(Uri uri) {
|
||||
return uri != null && MAILTO_SCHEME.equals(uri.getScheme());
|
||||
}
|
||||
|
||||
public static MailTo parse(Uri uri) throws NullPointerException, IllegalArgumentException {
|
||||
if (uri == null || uri.toString() == null) {
|
||||
throw new NullPointerException("Argument 'uri' must not be null");
|
||||
}
|
||||
|
||||
if (!isMailTo(uri)) {
|
||||
throw new IllegalArgumentException("Not a mailto scheme");
|
||||
}
|
||||
|
||||
String schemaSpecific = uri.getSchemeSpecificPart();
|
||||
int end = schemaSpecific.indexOf('?');
|
||||
if (end == -1) {
|
||||
end = schemaSpecific.length();
|
||||
}
|
||||
|
||||
CaseInsensitiveParamWrapper params =
|
||||
new CaseInsensitiveParamWrapper(Uri.parse("foo://bar?" + uri.getEncodedQuery()));
|
||||
|
||||
// Extract the recipient's email address from the mailto URI if there's one.
|
||||
String recipient = Uri.decode(schemaSpecific.substring(0, end));
|
||||
|
||||
List<String> toList = params.getQueryParameters(TO);
|
||||
if (recipient.length() != 0) {
|
||||
toList.add(0, recipient);
|
||||
}
|
||||
|
||||
List<String> ccList = params.getQueryParameters(CC);
|
||||
List<String> bccList = params.getQueryParameters(BCC);
|
||||
|
||||
Address[] toAddresses = toAddressArray(toList);
|
||||
Address[] ccAddresses = toAddressArray(ccList);
|
||||
Address[] bccAddresses = toAddressArray(bccList);
|
||||
|
||||
String subject = getFirstParameterValue(params, SUBJECT);
|
||||
String body = getFirstParameterValue(params, BODY);
|
||||
String inReplyTo = getFirstParameterValue(params, IN_REPLY_TO);
|
||||
|
||||
String inReplyToMessageId = null;
|
||||
if (inReplyTo != null) {
|
||||
try {
|
||||
List<String> inReplyToMessageIds = MessageIdParser.parseList(inReplyTo);
|
||||
inReplyToMessageId = inReplyToMessageIds.get(0);
|
||||
} catch (MimeHeaderParserException e) {
|
||||
Timber.w(e, "Ignoring invalid in-reply-to value within the mailto: link.");
|
||||
}
|
||||
}
|
||||
|
||||
return new MailTo(toAddresses, ccAddresses, bccAddresses, inReplyToMessageId, subject, body);
|
||||
}
|
||||
|
||||
private static String getFirstParameterValue(CaseInsensitiveParamWrapper params, String paramName) {
|
||||
List<String> paramValues = params.getQueryParameters(paramName);
|
||||
|
||||
return (paramValues.isEmpty()) ? null : paramValues.get(0);
|
||||
}
|
||||
|
||||
private static Address[] toAddressArray(List<String> recipients) {
|
||||
if (recipients.isEmpty()) {
|
||||
return EMPTY_ADDRESS_LIST;
|
||||
}
|
||||
|
||||
String addressList = toCommaSeparatedString(recipients);
|
||||
return Address.parse(addressList);
|
||||
}
|
||||
|
||||
private static String toCommaSeparatedString(List<String> list) {
|
||||
StringBuilder stringBuilder = new StringBuilder();
|
||||
for (String item : list) {
|
||||
stringBuilder.append(item).append(',');
|
||||
}
|
||||
|
||||
stringBuilder.setLength(stringBuilder.length() - 1);
|
||||
return stringBuilder.toString();
|
||||
}
|
||||
|
||||
private MailTo(Address[] toAddresses, Address[] ccAddresses, Address[] bccAddresses, String inReplyToMessageId,
|
||||
String subject, String body) {
|
||||
this.toAddresses = toAddresses;
|
||||
this.ccAddresses = ccAddresses;
|
||||
this.bccAddresses = bccAddresses;
|
||||
this.inReplyToMessageId = inReplyToMessageId;
|
||||
this.subject = subject;
|
||||
this.body = body;
|
||||
}
|
||||
|
||||
public Address[] getTo() {
|
||||
return toAddresses;
|
||||
}
|
||||
|
||||
public Address[] getCc() {
|
||||
return ccAddresses;
|
||||
}
|
||||
|
||||
public Address[] getBcc() {
|
||||
return bccAddresses;
|
||||
}
|
||||
|
||||
public String getSubject() {
|
||||
return subject;
|
||||
}
|
||||
|
||||
public String getBody() {
|
||||
return body;
|
||||
}
|
||||
|
||||
public String getInReplyTo() { return inReplyToMessageId; }
|
||||
|
||||
static class CaseInsensitiveParamWrapper {
|
||||
private final Uri uri;
|
||||
|
||||
|
||||
public CaseInsensitiveParamWrapper(Uri uri) {
|
||||
this.uri = uri;
|
||||
}
|
||||
|
||||
public List<String> getQueryParameters(String key) {
|
||||
List<String> params = new ArrayList<>();
|
||||
for (String paramName : uri.getQueryParameterNames()) {
|
||||
if (paramName.equalsIgnoreCase(key)) {
|
||||
params.addAll(uri.getQueryParameters(paramName));
|
||||
}
|
||||
}
|
||||
|
||||
return params;
|
||||
}
|
||||
}
|
||||
}
|
||||
131
app/core/src/main/java/com/fsck/k9/helper/MessageHelper.kt
Normal file
131
app/core/src/main/java/com/fsck/k9/helper/MessageHelper.kt
Normal file
|
|
@ -0,0 +1,131 @@
|
|||
package com.fsck.k9.helper
|
||||
|
||||
import android.text.Spannable
|
||||
import android.text.SpannableString
|
||||
import android.text.SpannableStringBuilder
|
||||
import android.text.TextUtils
|
||||
import android.text.style.ForegroundColorSpan
|
||||
import app.k9mail.core.android.common.contact.ContactRepository
|
||||
import app.k9mail.core.common.mail.EmailAddress
|
||||
import com.fsck.k9.CoreResourceProvider
|
||||
import com.fsck.k9.K9.contactNameColor
|
||||
import com.fsck.k9.K9.isChangeContactNameColor
|
||||
import com.fsck.k9.K9.isShowContactName
|
||||
import com.fsck.k9.K9.isShowCorrespondentNames
|
||||
import com.fsck.k9.mail.Address
|
||||
import java.util.regex.Pattern
|
||||
|
||||
class MessageHelper(
|
||||
private val resourceProvider: CoreResourceProvider,
|
||||
private val contactRepository: ContactRepository,
|
||||
) {
|
||||
|
||||
fun getSenderDisplayName(address: Address?): CharSequence {
|
||||
if (address == null) {
|
||||
return resourceProvider.contactUnknownSender()
|
||||
}
|
||||
val repository = if (isShowContactName) contactRepository else null
|
||||
return toFriendly(address, repository)
|
||||
}
|
||||
|
||||
fun getRecipientDisplayNames(addresses: Array<Address>?): CharSequence {
|
||||
if (addresses == null || addresses.isEmpty()) {
|
||||
return resourceProvider.contactUnknownRecipient()
|
||||
}
|
||||
val repository = if (isShowContactName) contactRepository else null
|
||||
val recipients = toFriendly(addresses, repository)
|
||||
return SpannableStringBuilder(resourceProvider.contactDisplayNamePrefix()).append(recipients)
|
||||
}
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* If the number of addresses exceeds this value the addresses aren't
|
||||
* resolved to the names of Android contacts.
|
||||
*
|
||||
* TODO: This number was chosen arbitrarily and should be determined by performance tests.
|
||||
*
|
||||
* @see .toFriendly
|
||||
*/
|
||||
private const val TOO_MANY_ADDRESSES = 50
|
||||
private val SPOOF_ADDRESS_PATTERN = Pattern.compile("[^(]@")
|
||||
|
||||
/**
|
||||
* Returns the name of the contact this email address belongs to if
|
||||
* the [contacts][Contacts] parameter is not `null` and a
|
||||
* contact is found. Otherwise the personal portion of the [Address]
|
||||
* is returned. If that isn't available either, the email address is
|
||||
* returned.
|
||||
*
|
||||
* @param address An [com.fsck.k9.mail.Address]
|
||||
* @param contacts A [Contacts] instance or `null`.
|
||||
* @return A "friendly" name for this [Address].
|
||||
*/
|
||||
fun toFriendly(address: Address, contactRepository: ContactRepository?): CharSequence {
|
||||
return toFriendly(
|
||||
address,
|
||||
contactRepository,
|
||||
isShowCorrespondentNames,
|
||||
isChangeContactNameColor,
|
||||
contactNameColor,
|
||||
)
|
||||
}
|
||||
|
||||
fun toFriendly(addresses: Array<Address>?, contactRepository: ContactRepository?): CharSequence? {
|
||||
var repository = contactRepository
|
||||
if (addresses == null) {
|
||||
return null
|
||||
}
|
||||
if (addresses.size >= TOO_MANY_ADDRESSES) {
|
||||
// Don't look up contacts if the number of addresses is very high.
|
||||
repository = null
|
||||
}
|
||||
val stringBuilder = SpannableStringBuilder()
|
||||
for (i in addresses.indices) {
|
||||
stringBuilder.append(toFriendly(addresses[i], repository))
|
||||
if (i < addresses.size - 1) {
|
||||
stringBuilder.append(',')
|
||||
}
|
||||
}
|
||||
return stringBuilder
|
||||
}
|
||||
|
||||
/* package, for testing */
|
||||
@JvmStatic
|
||||
fun toFriendly(
|
||||
address: Address,
|
||||
contactRepository: ContactRepository?,
|
||||
showCorrespondentNames: Boolean,
|
||||
changeContactNameColor: Boolean,
|
||||
contactNameColor: Int,
|
||||
): CharSequence {
|
||||
if (!showCorrespondentNames) {
|
||||
return address.address
|
||||
} else if (contactRepository != null) {
|
||||
val name = contactRepository.getContactFor(EmailAddress(address.address))?.name
|
||||
if (name != null) {
|
||||
return if (changeContactNameColor) {
|
||||
val coloredName = SpannableString(name)
|
||||
coloredName.setSpan(
|
||||
ForegroundColorSpan(contactNameColor),
|
||||
0,
|
||||
coloredName.length,
|
||||
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE,
|
||||
)
|
||||
coloredName
|
||||
} else {
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
return if (!TextUtils.isEmpty(address.personal) && !isSpoofAddress(address.personal)) {
|
||||
address.personal
|
||||
} else {
|
||||
address.address
|
||||
}
|
||||
}
|
||||
|
||||
private fun isSpoofAddress(displayName: String): Boolean {
|
||||
return displayName.contains("@") && SPOOF_ADDRESS_PATTERN.matcher(displayName).find()
|
||||
}
|
||||
}
|
||||
}
|
||||
925
app/core/src/main/java/com/fsck/k9/helper/MimeTypeUtil.java
Normal file
925
app/core/src/main/java/com/fsck/k9/helper/MimeTypeUtil.java
Normal file
|
|
@ -0,0 +1,925 @@
|
|||
package com.fsck.k9.helper;
|
||||
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
|
||||
public class MimeTypeUtil {
|
||||
public static final String DEFAULT_ATTACHMENT_MIME_TYPE = "application/octet-stream";
|
||||
public static final String K9_SETTINGS_MIME_TYPE = "application/x-k9settings";
|
||||
|
||||
/*
|
||||
* http://www.w3schools.com/media/media_mimeref.asp
|
||||
* +
|
||||
* http://www.stdicon.com/mimetypes
|
||||
*/
|
||||
static final String[][] MIME_TYPE_BY_EXTENSION_MAP = new String[][] {
|
||||
//* Do not delete the next three lines
|
||||
{ "", DEFAULT_ATTACHMENT_MIME_TYPE },
|
||||
{ "k9s", K9_SETTINGS_MIME_TYPE },
|
||||
{ "txt", "text/plain" },
|
||||
//* Do not delete the previous three lines
|
||||
{ "123", "application/vnd.lotus-1-2-3" },
|
||||
{ "323", "text/h323" },
|
||||
{ "3dml", "text/vnd.in3d.3dml" },
|
||||
{ "3g2", "video/3gpp2" },
|
||||
{ "3gp", "video/3gpp" },
|
||||
{ "aab", "application/x-authorware-bin" },
|
||||
{ "aac", "audio/x-aac" },
|
||||
{ "aam", "application/x-authorware-map" },
|
||||
{ "a", "application/octet-stream" },
|
||||
{ "aas", "application/x-authorware-seg" },
|
||||
{ "abw", "application/x-abiword" },
|
||||
{ "acc", "application/vnd.americandynamics.acc" },
|
||||
{ "ace", "application/x-ace-compressed" },
|
||||
{ "acu", "application/vnd.acucobol" },
|
||||
{ "acutc", "application/vnd.acucorp" },
|
||||
{ "acx", "application/internet-property-stream" },
|
||||
{ "adp", "audio/adpcm" },
|
||||
{ "aep", "application/vnd.audiograph" },
|
||||
{ "afm", "application/x-font-type1" },
|
||||
{ "afp", "application/vnd.ibm.modcap" },
|
||||
{ "ai", "application/postscript" },
|
||||
{ "aif", "audio/x-aiff" },
|
||||
{ "aifc", "audio/x-aiff" },
|
||||
{ "aiff", "audio/x-aiff" },
|
||||
{ "air", "application/vnd.adobe.air-application-installer-package+zip" },
|
||||
{ "ami", "application/vnd.amiga.ami" },
|
||||
{ "apk", "application/vnd.android.package-archive" },
|
||||
{ "application", "application/x-ms-application" },
|
||||
{ "apr", "application/vnd.lotus-approach" },
|
||||
{ "asc", "application/pgp-signature" },
|
||||
{ "asf", "video/x-ms-asf" },
|
||||
{ "asm", "text/x-asm" },
|
||||
{ "aso", "application/vnd.accpac.simply.aso" },
|
||||
{ "asr", "video/x-ms-asf" },
|
||||
{ "asx", "video/x-ms-asf" },
|
||||
{ "atc", "application/vnd.acucorp" },
|
||||
{ "atom", "application/atom+xml" },
|
||||
{ "atomcat", "application/atomcat+xml" },
|
||||
{ "atomsvc", "application/atomsvc+xml" },
|
||||
{ "atx", "application/vnd.antix.game-component" },
|
||||
{ "au", "audio/basic" },
|
||||
{ "avi", "video/x-msvideo" },
|
||||
{ "aw", "application/applixware" },
|
||||
{ "axs", "application/olescript" },
|
||||
{ "azf", "application/vnd.airzip.filesecure.azf" },
|
||||
{ "azs", "application/vnd.airzip.filesecure.azs" },
|
||||
{ "azw", "application/vnd.amazon.ebook" },
|
||||
{ "bas", "text/plain" },
|
||||
{ "bat", "application/x-msdownload" },
|
||||
{ "bcpio", "application/x-bcpio" },
|
||||
{ "bdf", "application/x-font-bdf" },
|
||||
{ "bdm", "application/vnd.syncml.dm+wbxml" },
|
||||
{ "bh2", "application/vnd.fujitsu.oasysprs" },
|
||||
{ "bin", "application/octet-stream" },
|
||||
{ "bmi", "application/vnd.bmi" },
|
||||
{ "bmp", "image/bmp" },
|
||||
{ "book", "application/vnd.framemaker" },
|
||||
{ "box", "application/vnd.previewsystems.box" },
|
||||
{ "boz", "application/x-bzip2" },
|
||||
{ "bpk", "application/octet-stream" },
|
||||
{ "btif", "image/prs.btif" },
|
||||
{ "bz2", "application/x-bzip2" },
|
||||
{ "bz", "application/x-bzip" },
|
||||
{ "c4d", "application/vnd.clonk.c4group" },
|
||||
{ "c4f", "application/vnd.clonk.c4group" },
|
||||
{ "c4g", "application/vnd.clonk.c4group" },
|
||||
{ "c4p", "application/vnd.clonk.c4group" },
|
||||
{ "c4u", "application/vnd.clonk.c4group" },
|
||||
{ "cab", "application/vnd.ms-cab-compressed" },
|
||||
{ "car", "application/vnd.curl.car" },
|
||||
{ "cat", "application/vnd.ms-pki.seccat" },
|
||||
{ "cct", "application/x-director" },
|
||||
{ "cc", "text/x-c" },
|
||||
{ "ccxml", "application/ccxml+xml" },
|
||||
{ "cdbcmsg", "application/vnd.contact.cmsg" },
|
||||
{ "cdf", "application/x-cdf" },
|
||||
{ "cdkey", "application/vnd.mediastation.cdkey" },
|
||||
{ "cdx", "chemical/x-cdx" },
|
||||
{ "cdxml", "application/vnd.chemdraw+xml" },
|
||||
{ "cdy", "application/vnd.cinderella" },
|
||||
{ "cer", "application/x-x509-ca-cert" },
|
||||
{ "cgm", "image/cgm" },
|
||||
{ "chat", "application/x-chat" },
|
||||
{ "chm", "application/vnd.ms-htmlhelp" },
|
||||
{ "chrt", "application/vnd.kde.kchart" },
|
||||
{ "cif", "chemical/x-cif" },
|
||||
{ "cii", "application/vnd.anser-web-certificate-issue-initiation" },
|
||||
{ "cla", "application/vnd.claymore" },
|
||||
{ "class", "application/java-vm" },
|
||||
{ "clkk", "application/vnd.crick.clicker.keyboard" },
|
||||
{ "clkp", "application/vnd.crick.clicker.palette" },
|
||||
{ "clkt", "application/vnd.crick.clicker.template" },
|
||||
{ "clkw", "application/vnd.crick.clicker.wordbank" },
|
||||
{ "clkx", "application/vnd.crick.clicker" },
|
||||
{ "clp", "application/x-msclip" },
|
||||
{ "cmc", "application/vnd.cosmocaller" },
|
||||
{ "cmdf", "chemical/x-cmdf" },
|
||||
{ "cml", "chemical/x-cml" },
|
||||
{ "cmp", "application/vnd.yellowriver-custom-menu" },
|
||||
{ "cmx", "image/x-cmx" },
|
||||
{ "cod", "application/vnd.rim.cod" },
|
||||
{ "com", "application/x-msdownload" },
|
||||
{ "conf", "text/plain" },
|
||||
{ "cpio", "application/x-cpio" },
|
||||
{ "cpp", "text/x-c" },
|
||||
{ "cpt", "application/mac-compactpro" },
|
||||
{ "crd", "application/x-mscardfile" },
|
||||
{ "crl", "application/pkix-crl" },
|
||||
{ "crt", "application/x-x509-ca-cert" },
|
||||
{ "csh", "application/x-csh" },
|
||||
{ "csml", "chemical/x-csml" },
|
||||
{ "csp", "application/vnd.commonspace" },
|
||||
{ "css", "text/css" },
|
||||
{ "cst", "application/x-director" },
|
||||
{ "csv", "text/csv" },
|
||||
{ "c", "text/plain" },
|
||||
{ "cu", "application/cu-seeme" },
|
||||
{ "curl", "text/vnd.curl" },
|
||||
{ "cww", "application/prs.cww" },
|
||||
{ "cxt", "application/x-director" },
|
||||
{ "cxx", "text/x-c" },
|
||||
{ "daf", "application/vnd.mobius.daf" },
|
||||
{ "dataless", "application/vnd.fdsn.seed" },
|
||||
{ "davmount", "application/davmount+xml" },
|
||||
{ "dcr", "application/x-director" },
|
||||
{ "dcurl", "text/vnd.curl.dcurl" },
|
||||
{ "dd2", "application/vnd.oma.dd2+xml" },
|
||||
{ "ddd", "application/vnd.fujixerox.ddd" },
|
||||
{ "deb", "application/x-debian-package" },
|
||||
{ "def", "text/plain" },
|
||||
{ "deploy", "application/octet-stream" },
|
||||
{ "der", "application/x-x509-ca-cert" },
|
||||
{ "dfac", "application/vnd.dreamfactory" },
|
||||
{ "dic", "text/x-c" },
|
||||
{ "diff", "text/plain" },
|
||||
{ "dir", "application/x-director" },
|
||||
{ "dis", "application/vnd.mobius.dis" },
|
||||
{ "dist", "application/octet-stream" },
|
||||
{ "distz", "application/octet-stream" },
|
||||
{ "djv", "image/vnd.djvu" },
|
||||
{ "djvu", "image/vnd.djvu" },
|
||||
{ "dll", "application/x-msdownload" },
|
||||
{ "dmg", "application/octet-stream" },
|
||||
{ "dms", "application/octet-stream" },
|
||||
{ "dna", "application/vnd.dna" },
|
||||
{ "doc", "application/msword" },
|
||||
{ "docm", "application/vnd.ms-word.document.macroenabled.12" },
|
||||
{ "docx", "application/vnd.openxmlformats-officedocument.wordprocessingml.document" },
|
||||
{ "dot", "application/msword" },
|
||||
{ "dotm", "application/vnd.ms-word.template.macroenabled.12" },
|
||||
{ "dotx", "application/vnd.openxmlformats-officedocument.wordprocessingml.template" },
|
||||
{ "dp", "application/vnd.osgi.dp" },
|
||||
{ "dpg", "application/vnd.dpgraph" },
|
||||
{ "dsc", "text/prs.lines.tag" },
|
||||
{ "dtb", "application/x-dtbook+xml" },
|
||||
{ "dtd", "application/xml-dtd" },
|
||||
{ "dts", "audio/vnd.dts" },
|
||||
{ "dtshd", "audio/vnd.dts.hd" },
|
||||
{ "dump", "application/octet-stream" },
|
||||
{ "dvi", "application/x-dvi" },
|
||||
{ "dwf", "model/vnd.dwf" },
|
||||
{ "dwg", "image/vnd.dwg" },
|
||||
{ "dxf", "image/vnd.dxf" },
|
||||
{ "dxp", "application/vnd.spotfire.dxp" },
|
||||
{ "dxr", "application/x-director" },
|
||||
{ "ecelp4800", "audio/vnd.nuera.ecelp4800" },
|
||||
{ "ecelp7470", "audio/vnd.nuera.ecelp7470" },
|
||||
{ "ecelp9600", "audio/vnd.nuera.ecelp9600" },
|
||||
{ "ecma", "application/ecmascript" },
|
||||
{ "edm", "application/vnd.novadigm.edm" },
|
||||
{ "edx", "application/vnd.novadigm.edx" },
|
||||
{ "efif", "application/vnd.picsel" },
|
||||
{ "ei6", "application/vnd.pg.osasli" },
|
||||
{ "elc", "application/octet-stream" },
|
||||
{ "eml", "message/rfc822" },
|
||||
{ "emma", "application/emma+xml" },
|
||||
{ "eol", "audio/vnd.digital-winds" },
|
||||
{ "eot", "application/vnd.ms-fontobject" },
|
||||
{ "eps", "application/postscript" },
|
||||
{ "epub", "application/epub+zip" },
|
||||
{ "es3", "application/vnd.eszigno3+xml" },
|
||||
{ "esf", "application/vnd.epson.esf" },
|
||||
{ "espass", "application/vnd.espass-espass+zip" },
|
||||
{ "et3", "application/vnd.eszigno3+xml" },
|
||||
{ "etx", "text/x-setext" },
|
||||
{ "evy", "application/envoy" },
|
||||
{ "exe", "application/octet-stream" },
|
||||
{ "ext", "application/vnd.novadigm.ext" },
|
||||
{ "ez2", "application/vnd.ezpix-album" },
|
||||
{ "ez3", "application/vnd.ezpix-package" },
|
||||
{ "ez", "application/andrew-inset" },
|
||||
{ "f4v", "video/x-f4v" },
|
||||
{ "f77", "text/x-fortran" },
|
||||
{ "f90", "text/x-fortran" },
|
||||
{ "fbs", "image/vnd.fastbidsheet" },
|
||||
{ "fdf", "application/vnd.fdf" },
|
||||
{ "fe_launch", "application/vnd.denovo.fcselayout-link" },
|
||||
{ "fg5", "application/vnd.fujitsu.oasysgp" },
|
||||
{ "fgd", "application/x-director" },
|
||||
{ "fh4", "image/x-freehand" },
|
||||
{ "fh5", "image/x-freehand" },
|
||||
{ "fh7", "image/x-freehand" },
|
||||
{ "fhc", "image/x-freehand" },
|
||||
{ "fh", "image/x-freehand" },
|
||||
{ "fif", "application/fractals" },
|
||||
{ "fig", "application/x-xfig" },
|
||||
{ "fli", "video/x-fli" },
|
||||
{ "flo", "application/vnd.micrografx.flo" },
|
||||
{ "flr", "x-world/x-vrml" },
|
||||
{ "flv", "video/x-flv" },
|
||||
{ "flw", "application/vnd.kde.kivio" },
|
||||
{ "flx", "text/vnd.fmi.flexstor" },
|
||||
{ "fly", "text/vnd.fly" },
|
||||
{ "fm", "application/vnd.framemaker" },
|
||||
{ "fnc", "application/vnd.frogans.fnc" },
|
||||
{ "for", "text/x-fortran" },
|
||||
{ "fpx", "image/vnd.fpx" },
|
||||
{ "frame", "application/vnd.framemaker" },
|
||||
{ "fsc", "application/vnd.fsc.weblaunch" },
|
||||
{ "fst", "image/vnd.fst" },
|
||||
{ "ftc", "application/vnd.fluxtime.clip" },
|
||||
{ "f", "text/x-fortran" },
|
||||
{ "fti", "application/vnd.anser-web-funds-transfer-initiation" },
|
||||
{ "fvt", "video/vnd.fvt" },
|
||||
{ "fzs", "application/vnd.fuzzysheet" },
|
||||
{ "g3", "image/g3fax" },
|
||||
{ "gac", "application/vnd.groove-account" },
|
||||
{ "gdl", "model/vnd.gdl" },
|
||||
{ "geo", "application/vnd.dynageo" },
|
||||
{ "gex", "application/vnd.geometry-explorer" },
|
||||
{ "ggb", "application/vnd.geogebra.file" },
|
||||
{ "ggt", "application/vnd.geogebra.tool" },
|
||||
{ "ghf", "application/vnd.groove-help" },
|
||||
{ "gif", "image/gif" },
|
||||
{ "gim", "application/vnd.groove-identity-message" },
|
||||
{ "gmx", "application/vnd.gmx" },
|
||||
{ "gnumeric", "application/x-gnumeric" },
|
||||
{ "gph", "application/vnd.flographit" },
|
||||
{ "gqf", "application/vnd.grafeq" },
|
||||
{ "gqs", "application/vnd.grafeq" },
|
||||
{ "gram", "application/srgs" },
|
||||
{ "gre", "application/vnd.geometry-explorer" },
|
||||
{ "grv", "application/vnd.groove-injector" },
|
||||
{ "grxml", "application/srgs+xml" },
|
||||
{ "gsf", "application/x-font-ghostscript" },
|
||||
{ "gtar", "application/x-gtar" },
|
||||
{ "gtm", "application/vnd.groove-tool-message" },
|
||||
{ "gtw", "model/vnd.gtw" },
|
||||
{ "gv", "text/vnd.graphviz" },
|
||||
{ "gz", "application/x-gzip" },
|
||||
{ "h261", "video/h261" },
|
||||
{ "h263", "video/h263" },
|
||||
{ "h264", "video/h264" },
|
||||
{ "hbci", "application/vnd.hbci" },
|
||||
{ "hdf", "application/x-hdf" },
|
||||
{ "hh", "text/x-c" },
|
||||
{ "hlp", "application/winhlp" },
|
||||
{ "hpgl", "application/vnd.hp-hpgl" },
|
||||
{ "hpid", "application/vnd.hp-hpid" },
|
||||
{ "hps", "application/vnd.hp-hps" },
|
||||
{ "hqx", "application/mac-binhex40" },
|
||||
{ "hta", "application/hta" },
|
||||
{ "htc", "text/x-component" },
|
||||
{ "h", "text/plain" },
|
||||
{ "htke", "application/vnd.kenameaapp" },
|
||||
{ "html", "text/html" },
|
||||
{ "htm", "text/html" },
|
||||
{ "htt", "text/webviewhtml" },
|
||||
{ "hvd", "application/vnd.yamaha.hv-dic" },
|
||||
{ "hvp", "application/vnd.yamaha.hv-voice" },
|
||||
{ "hvs", "application/vnd.yamaha.hv-script" },
|
||||
{ "icc", "application/vnd.iccprofile" },
|
||||
{ "ice", "x-conference/x-cooltalk" },
|
||||
{ "icm", "application/vnd.iccprofile" },
|
||||
{ "ico", "image/x-icon" },
|
||||
{ "ics", "text/calendar" },
|
||||
{ "ief", "image/ief" },
|
||||
{ "ifb", "text/calendar" },
|
||||
{ "ifm", "application/vnd.shana.informed.formdata" },
|
||||
{ "iges", "model/iges" },
|
||||
{ "igl", "application/vnd.igloader" },
|
||||
{ "igs", "model/iges" },
|
||||
{ "igx", "application/vnd.micrografx.igx" },
|
||||
{ "iif", "application/vnd.shana.informed.interchange" },
|
||||
{ "iii", "application/x-iphone" },
|
||||
{ "imp", "application/vnd.accpac.simply.imp" },
|
||||
{ "ims", "application/vnd.ms-ims" },
|
||||
{ "ins", "application/x-internet-signup" },
|
||||
{ "in", "text/plain" },
|
||||
{ "ipk", "application/vnd.shana.informed.package" },
|
||||
{ "irm", "application/vnd.ibm.rights-management" },
|
||||
{ "irp", "application/vnd.irepository.package+xml" },
|
||||
{ "iso", "application/octet-stream" },
|
||||
{ "isp", "application/x-internet-signup" },
|
||||
{ "itp", "application/vnd.shana.informed.formtemplate" },
|
||||
{ "ivp", "application/vnd.immervision-ivp" },
|
||||
{ "ivu", "application/vnd.immervision-ivu" },
|
||||
{ "jad", "text/vnd.sun.j2me.app-descriptor" },
|
||||
{ "jam", "application/vnd.jam" },
|
||||
{ "jar", "application/java-archive" },
|
||||
{ "java", "text/x-java-source" },
|
||||
{ "jfif", "image/pipeg" },
|
||||
{ "jisp", "application/vnd.jisp" },
|
||||
{ "jlt", "application/vnd.hp-jlyt" },
|
||||
{ "jnlp", "application/x-java-jnlp-file" },
|
||||
{ "joda", "application/vnd.joost.joda-archive" },
|
||||
{ "jpeg", "image/jpeg" },
|
||||
{ "jpe", "image/jpeg" },
|
||||
{ "jpg", "image/jpeg" },
|
||||
{ "jpgm", "video/jpm" },
|
||||
{ "jpgv", "video/jpeg" },
|
||||
{ "jpm", "video/jpm" },
|
||||
{ "js", "application/x-javascript" },
|
||||
{ "json", "application/json" },
|
||||
{ "kar", "audio/midi" },
|
||||
{ "karbon", "application/vnd.kde.karbon" },
|
||||
{ "kfo", "application/vnd.kde.kformula" },
|
||||
{ "kia", "application/vnd.kidspiration" },
|
||||
{ "kil", "application/x-killustrator" },
|
||||
{ "kml", "application/vnd.google-earth.kml+xml" },
|
||||
{ "kmz", "application/vnd.google-earth.kmz" },
|
||||
{ "kne", "application/vnd.kinar" },
|
||||
{ "knp", "application/vnd.kinar" },
|
||||
{ "kon", "application/vnd.kde.kontour" },
|
||||
{ "kpr", "application/vnd.kde.kpresenter" },
|
||||
{ "kpt", "application/vnd.kde.kpresenter" },
|
||||
{ "ksh", "text/plain" },
|
||||
{ "ksp", "application/vnd.kde.kspread" },
|
||||
{ "ktr", "application/vnd.kahootz" },
|
||||
{ "ktz", "application/vnd.kahootz" },
|
||||
{ "kwd", "application/vnd.kde.kword" },
|
||||
{ "kwt", "application/vnd.kde.kword" },
|
||||
{ "latex", "application/x-latex" },
|
||||
{ "lbd", "application/vnd.llamagraphics.life-balance.desktop" },
|
||||
{ "lbe", "application/vnd.llamagraphics.life-balance.exchange+xml" },
|
||||
{ "les", "application/vnd.hhe.lesson-player" },
|
||||
{ "lha", "application/octet-stream" },
|
||||
{ "link66", "application/vnd.route66.link66+xml" },
|
||||
{ "list3820", "application/vnd.ibm.modcap" },
|
||||
{ "listafp", "application/vnd.ibm.modcap" },
|
||||
{ "list", "text/plain" },
|
||||
{ "log", "text/plain" },
|
||||
{ "lostxml", "application/lost+xml" },
|
||||
{ "lrf", "application/octet-stream" },
|
||||
{ "lrm", "application/vnd.ms-lrm" },
|
||||
{ "lsf", "video/x-la-asf" },
|
||||
{ "lsx", "video/x-la-asf" },
|
||||
{ "ltf", "application/vnd.frogans.ltf" },
|
||||
{ "lvp", "audio/vnd.lucent.voice" },
|
||||
{ "lwp", "application/vnd.lotus-wordpro" },
|
||||
{ "lzh", "application/octet-stream" },
|
||||
{ "m13", "application/x-msmediaview" },
|
||||
{ "m14", "application/x-msmediaview" },
|
||||
{ "m1v", "video/mpeg" },
|
||||
{ "m2a", "audio/mpeg" },
|
||||
{ "m2v", "video/mpeg" },
|
||||
{ "m3a", "audio/mpeg" },
|
||||
{ "m3u", "audio/x-mpegurl" },
|
||||
{ "m4u", "video/vnd.mpegurl" },
|
||||
{ "m4v", "video/x-m4v" },
|
||||
{ "ma", "application/mathematica" },
|
||||
{ "mag", "application/vnd.ecowin.chart" },
|
||||
{ "maker", "application/vnd.framemaker" },
|
||||
{ "man", "text/troff" },
|
||||
{ "mathml", "application/mathml+xml" },
|
||||
{ "mb", "application/mathematica" },
|
||||
{ "mbk", "application/vnd.mobius.mbk" },
|
||||
{ "mbox", "application/mbox" },
|
||||
{ "mc1", "application/vnd.medcalcdata" },
|
||||
{ "mcd", "application/vnd.mcd" },
|
||||
{ "mcurl", "text/vnd.curl.mcurl" },
|
||||
{ "mdb", "application/x-msaccess" },
|
||||
{ "mdi", "image/vnd.ms-modi" },
|
||||
{ "mesh", "model/mesh" },
|
||||
{ "me", "text/troff" },
|
||||
{ "mfm", "application/vnd.mfmp" },
|
||||
{ "mgz", "application/vnd.proteus.magazine" },
|
||||
{ "mht", "message/rfc822" },
|
||||
{ "mhtml", "message/rfc822" },
|
||||
{ "mid", "audio/midi" },
|
||||
{ "midi", "audio/midi" },
|
||||
{ "mif", "application/vnd.mif" },
|
||||
{ "mime", "message/rfc822" },
|
||||
{ "mj2", "video/mj2" },
|
||||
{ "mjp2", "video/mj2" },
|
||||
{ "mlp", "application/vnd.dolby.mlp" },
|
||||
{ "mmd", "application/vnd.chipnuts.karaoke-mmd" },
|
||||
{ "mmf", "application/vnd.smaf" },
|
||||
{ "mmr", "image/vnd.fujixerox.edmics-mmr" },
|
||||
{ "mny", "application/x-msmoney" },
|
||||
{ "mobi", "application/x-mobipocket-ebook" },
|
||||
{ "movie", "video/x-sgi-movie" },
|
||||
{ "mov", "video/quicktime" },
|
||||
{ "mp2a", "audio/mpeg" },
|
||||
{ "mp2", "video/mpeg" },
|
||||
{ "mp3", "audio/mpeg" },
|
||||
{ "mp4a", "audio/mp4" },
|
||||
{ "mp4s", "application/mp4" },
|
||||
{ "mp4", "video/mp4" },
|
||||
{ "mp4v", "video/mp4" },
|
||||
{ "mpa", "video/mpeg" },
|
||||
{ "mpc", "application/vnd.mophun.certificate" },
|
||||
{ "mpeg", "video/mpeg" },
|
||||
{ "mpe", "video/mpeg" },
|
||||
{ "mpg4", "video/mp4" },
|
||||
{ "mpga", "audio/mpeg" },
|
||||
{ "mpg", "video/mpeg" },
|
||||
{ "mpkg", "application/vnd.apple.installer+xml" },
|
||||
{ "mpm", "application/vnd.blueice.multipass" },
|
||||
{ "mpn", "application/vnd.mophun.application" },
|
||||
{ "mpp", "application/vnd.ms-project" },
|
||||
{ "mpt", "application/vnd.ms-project" },
|
||||
{ "mpv2", "video/mpeg" },
|
||||
{ "mpy", "application/vnd.ibm.minipay" },
|
||||
{ "mqy", "application/vnd.mobius.mqy" },
|
||||
{ "mrc", "application/marc" },
|
||||
{ "mscml", "application/mediaservercontrol+xml" },
|
||||
{ "mseed", "application/vnd.fdsn.mseed" },
|
||||
{ "mseq", "application/vnd.mseq" },
|
||||
{ "msf", "application/vnd.epson.msf" },
|
||||
{ "msh", "model/mesh" },
|
||||
{ "msi", "application/x-msdownload" },
|
||||
{ "ms", "text/troff" },
|
||||
{ "msty", "application/vnd.muvee.style" },
|
||||
{ "mts", "model/vnd.mts" },
|
||||
{ "mus", "application/vnd.musician" },
|
||||
{ "musicxml", "application/vnd.recordare.musicxml+xml" },
|
||||
{ "mvb", "application/x-msmediaview" },
|
||||
{ "mxf", "application/mxf" },
|
||||
{ "mxl", "application/vnd.recordare.musicxml" },
|
||||
{ "mxml", "application/xv+xml" },
|
||||
{ "mxs", "application/vnd.triscape.mxs" },
|
||||
{ "mxu", "video/vnd.mpegurl" },
|
||||
{ "nb", "application/mathematica" },
|
||||
{ "nc", "application/x-netcdf" },
|
||||
{ "ncx", "application/x-dtbncx+xml" },
|
||||
{ "n-gage", "application/vnd.nokia.n-gage.symbian.install" },
|
||||
{ "ngdat", "application/vnd.nokia.n-gage.data" },
|
||||
{ "nlu", "application/vnd.neurolanguage.nlu" },
|
||||
{ "nml", "application/vnd.enliven" },
|
||||
{ "nnd", "application/vnd.noblenet-directory" },
|
||||
{ "nns", "application/vnd.noblenet-sealer" },
|
||||
{ "nnw", "application/vnd.noblenet-web" },
|
||||
{ "npx", "image/vnd.net-fpx" },
|
||||
{ "nsf", "application/vnd.lotus-notes" },
|
||||
{ "nws", "message/rfc822" },
|
||||
{ "oa2", "application/vnd.fujitsu.oasys2" },
|
||||
{ "oa3", "application/vnd.fujitsu.oasys3" },
|
||||
{ "o", "application/octet-stream" },
|
||||
{ "oas", "application/vnd.fujitsu.oasys" },
|
||||
{ "obd", "application/x-msbinder" },
|
||||
{ "obj", "application/octet-stream" },
|
||||
{ "oda", "application/oda" },
|
||||
{ "odb", "application/vnd.oasis.opendocument.database" },
|
||||
{ "odc", "application/vnd.oasis.opendocument.chart" },
|
||||
{ "odf", "application/vnd.oasis.opendocument.formula" },
|
||||
{ "odft", "application/vnd.oasis.opendocument.formula-template" },
|
||||
{ "odg", "application/vnd.oasis.opendocument.graphics" },
|
||||
{ "odi", "application/vnd.oasis.opendocument.image" },
|
||||
{ "odp", "application/vnd.oasis.opendocument.presentation" },
|
||||
{ "ods", "application/vnd.oasis.opendocument.spreadsheet" },
|
||||
{ "odt", "application/vnd.oasis.opendocument.text" },
|
||||
{ "oga", "audio/ogg" },
|
||||
{ "ogg", "audio/ogg" },
|
||||
{ "ogv", "video/ogg" },
|
||||
{ "ogx", "application/ogg" },
|
||||
{ "onepkg", "application/onenote" },
|
||||
{ "onetmp", "application/onenote" },
|
||||
{ "onetoc2", "application/onenote" },
|
||||
{ "onetoc", "application/onenote" },
|
||||
{ "opf", "application/oebps-package+xml" },
|
||||
{ "oprc", "application/vnd.palm" },
|
||||
{ "org", "application/vnd.lotus-organizer" },
|
||||
{ "osf", "application/vnd.yamaha.openscoreformat" },
|
||||
{ "osfpvg", "application/vnd.yamaha.openscoreformat.osfpvg+xml" },
|
||||
{ "otc", "application/vnd.oasis.opendocument.chart-template" },
|
||||
{ "otf", "application/x-font-otf" },
|
||||
{ "otg", "application/vnd.oasis.opendocument.graphics-template" },
|
||||
{ "oth", "application/vnd.oasis.opendocument.text-web" },
|
||||
{ "oti", "application/vnd.oasis.opendocument.image-template" },
|
||||
{ "otm", "application/vnd.oasis.opendocument.text-master" },
|
||||
{ "otp", "application/vnd.oasis.opendocument.presentation-template" },
|
||||
{ "ots", "application/vnd.oasis.opendocument.spreadsheet-template" },
|
||||
{ "ott", "application/vnd.oasis.opendocument.text-template" },
|
||||
{ "oxt", "application/vnd.openofficeorg.extension" },
|
||||
{ "p10", "application/pkcs10" },
|
||||
{ "p12", "application/x-pkcs12" },
|
||||
{ "p7b", "application/x-pkcs7-certificates" },
|
||||
{ "p7c", "application/x-pkcs7-mime" },
|
||||
{ "p7m", "application/x-pkcs7-mime" },
|
||||
{ "p7r", "application/x-pkcs7-certreqresp" },
|
||||
{ "p7s", "application/x-pkcs7-signature" },
|
||||
{ "pas", "text/x-pascal" },
|
||||
{ "pbd", "application/vnd.powerbuilder6" },
|
||||
{ "pbm", "image/x-portable-bitmap" },
|
||||
{ "pcf", "application/x-font-pcf" },
|
||||
{ "pcl", "application/vnd.hp-pcl" },
|
||||
{ "pclxl", "application/vnd.hp-pclxl" },
|
||||
{ "pct", "image/x-pict" },
|
||||
{ "pcurl", "application/vnd.curl.pcurl" },
|
||||
{ "pcx", "image/x-pcx" },
|
||||
{ "pdb", "application/vnd.palm" },
|
||||
{ "pdf", "application/pdf" },
|
||||
{ "pfa", "application/x-font-type1" },
|
||||
{ "pfb", "application/x-font-type1" },
|
||||
{ "pfm", "application/x-font-type1" },
|
||||
{ "pfr", "application/font-tdpfr" },
|
||||
{ "pfx", "application/x-pkcs12" },
|
||||
{ "pgm", "image/x-portable-graymap" },
|
||||
{ "pgn", "application/x-chess-pgn" },
|
||||
{ "pgp", "application/pgp-encrypted" },
|
||||
{ "pic", "image/x-pict" },
|
||||
{ "pkg", "application/octet-stream" },
|
||||
{ "pki", "application/pkixcmp" },
|
||||
{ "pkipath", "application/pkix-pkipath" },
|
||||
{ "pkpass", "application/vnd-com.apple.pkpass" },
|
||||
{ "pko", "application/ynd.ms-pkipko" },
|
||||
{ "plb", "application/vnd.3gpp.pic-bw-large" },
|
||||
{ "plc", "application/vnd.mobius.plc" },
|
||||
{ "plf", "application/vnd.pocketlearn" },
|
||||
{ "pls", "application/pls+xml" },
|
||||
{ "pl", "text/plain" },
|
||||
{ "pma", "application/x-perfmon" },
|
||||
{ "pmc", "application/x-perfmon" },
|
||||
{ "pml", "application/x-perfmon" },
|
||||
{ "pmr", "application/x-perfmon" },
|
||||
{ "pmw", "application/x-perfmon" },
|
||||
{ "png", "image/png" },
|
||||
{ "pnm", "image/x-portable-anymap" },
|
||||
{ "portpkg", "application/vnd.macports.portpkg" },
|
||||
{ "pot,", "application/vnd.ms-powerpoint" },
|
||||
{ "pot", "application/vnd.ms-powerpoint" },
|
||||
{ "potm", "application/vnd.ms-powerpoint.template.macroenabled.12" },
|
||||
{ "potx", "application/vnd.openxmlformats-officedocument.presentationml.template" },
|
||||
{ "ppa", "application/vnd.ms-powerpoint" },
|
||||
{ "ppam", "application/vnd.ms-powerpoint.addin.macroenabled.12" },
|
||||
{ "ppd", "application/vnd.cups-ppd" },
|
||||
{ "ppm", "image/x-portable-pixmap" },
|
||||
{ "pps", "application/vnd.ms-powerpoint" },
|
||||
{ "ppsm", "application/vnd.ms-powerpoint.slideshow.macroenabled.12" },
|
||||
{ "ppsx", "application/vnd.openxmlformats-officedocument.presentationml.slideshow" },
|
||||
{ "ppt", "application/vnd.ms-powerpoint" },
|
||||
{ "pptm", "application/vnd.ms-powerpoint.presentation.macroenabled.12" },
|
||||
{ "pptx", "application/vnd.openxmlformats-officedocument.presentationml.presentation" },
|
||||
{ "pqa", "application/vnd.palm" },
|
||||
{ "prc", "application/x-mobipocket-ebook" },
|
||||
{ "pre", "application/vnd.lotus-freelance" },
|
||||
{ "prf", "application/pics-rules" },
|
||||
{ "ps", "application/postscript" },
|
||||
{ "psb", "application/vnd.3gpp.pic-bw-small" },
|
||||
{ "psd", "image/vnd.adobe.photoshop" },
|
||||
{ "psf", "application/x-font-linux-psf" },
|
||||
{ "p", "text/x-pascal" },
|
||||
{ "ptid", "application/vnd.pvi.ptid1" },
|
||||
{ "pub", "application/x-mspublisher" },
|
||||
{ "pvb", "application/vnd.3gpp.pic-bw-var" },
|
||||
{ "pwn", "application/vnd.3m.post-it-notes" },
|
||||
{ "pwz", "application/vnd.ms-powerpoint" },
|
||||
{ "pya", "audio/vnd.ms-playready.media.pya" },
|
||||
{ "pyc", "application/x-python-code" },
|
||||
{ "pyo", "application/x-python-code" },
|
||||
{ "py", "text/x-python" },
|
||||
{ "pyv", "video/vnd.ms-playready.media.pyv" },
|
||||
{ "qam", "application/vnd.epson.quickanime" },
|
||||
{ "qbo", "application/vnd.intu.qbo" },
|
||||
{ "qfx", "application/vnd.intu.qfx" },
|
||||
{ "qps", "application/vnd.publishare-delta-tree" },
|
||||
{ "qt", "video/quicktime" },
|
||||
{ "qwd", "application/vnd.quark.quarkxpress" },
|
||||
{ "qwt", "application/vnd.quark.quarkxpress" },
|
||||
{ "qxb", "application/vnd.quark.quarkxpress" },
|
||||
{ "qxd", "application/vnd.quark.quarkxpress" },
|
||||
{ "qxl", "application/vnd.quark.quarkxpress" },
|
||||
{ "qxt", "application/vnd.quark.quarkxpress" },
|
||||
{ "ra", "audio/x-pn-realaudio" },
|
||||
{ "ram", "audio/x-pn-realaudio" },
|
||||
{ "rar", "application/x-rar-compressed" },
|
||||
{ "ras", "image/x-cmu-raster" },
|
||||
{ "rcprofile", "application/vnd.ipunplugged.rcprofile" },
|
||||
{ "rdf", "application/rdf+xml" },
|
||||
{ "rdz", "application/vnd.data-vision.rdz" },
|
||||
{ "rep", "application/vnd.businessobjects" },
|
||||
{ "res", "application/x-dtbresource+xml" },
|
||||
{ "rgb", "image/x-rgb" },
|
||||
{ "rif", "application/reginfo+xml" },
|
||||
{ "rl", "application/resource-lists+xml" },
|
||||
{ "rlc", "image/vnd.fujixerox.edmics-rlc" },
|
||||
{ "rld", "application/resource-lists-diff+xml" },
|
||||
{ "rm", "application/vnd.rn-realmedia" },
|
||||
{ "rmi", "audio/midi" },
|
||||
{ "rmp", "audio/x-pn-realaudio-plugin" },
|
||||
{ "rms", "application/vnd.jcp.javame.midlet-rms" },
|
||||
{ "rnc", "application/relax-ng-compact-syntax" },
|
||||
{ "roff", "text/troff" },
|
||||
{ "rpm", "application/x-rpm" },
|
||||
{ "rpss", "application/vnd.nokia.radio-presets" },
|
||||
{ "rpst", "application/vnd.nokia.radio-preset" },
|
||||
{ "rq", "application/sparql-query" },
|
||||
{ "rs", "application/rls-services+xml" },
|
||||
{ "rsd", "application/rsd+xml" },
|
||||
{ "rss", "application/rss+xml" },
|
||||
{ "rtf", "application/rtf" },
|
||||
{ "rtx", "text/richtext" },
|
||||
{ "saf", "application/vnd.yamaha.smaf-audio" },
|
||||
{ "sbml", "application/sbml+xml" },
|
||||
{ "sc", "application/vnd.ibm.secure-container" },
|
||||
{ "scd", "application/x-msschedule" },
|
||||
{ "scm", "application/vnd.lotus-screencam" },
|
||||
{ "scq", "application/scvp-cv-request" },
|
||||
{ "scs", "application/scvp-cv-response" },
|
||||
{ "sct", "text/scriptlet" },
|
||||
{ "scurl", "text/vnd.curl.scurl" },
|
||||
{ "sda", "application/vnd.stardivision.draw" },
|
||||
{ "sdc", "application/vnd.stardivision.calc" },
|
||||
{ "sdd", "application/vnd.stardivision.impress" },
|
||||
{ "sdkd", "application/vnd.solent.sdkm+xml" },
|
||||
{ "sdkm", "application/vnd.solent.sdkm+xml" },
|
||||
{ "sdp", "application/sdp" },
|
||||
{ "sdw", "application/vnd.stardivision.writer" },
|
||||
{ "see", "application/vnd.seemail" },
|
||||
{ "seed", "application/vnd.fdsn.seed" },
|
||||
{ "sema", "application/vnd.sema" },
|
||||
{ "semd", "application/vnd.semd" },
|
||||
{ "semf", "application/vnd.semf" },
|
||||
{ "ser", "application/java-serialized-object" },
|
||||
{ "setpay", "application/set-payment-initiation" },
|
||||
{ "setreg", "application/set-registration-initiation" },
|
||||
{ "sfd-hdstx", "application/vnd.hydrostatix.sof-data" },
|
||||
{ "sfs", "application/vnd.spotfire.sfs" },
|
||||
{ "sgl", "application/vnd.stardivision.writer-global" },
|
||||
{ "sgml", "text/sgml" },
|
||||
{ "sgm", "text/sgml" },
|
||||
{ "sh", "application/x-sh" },
|
||||
{ "shar", "application/x-shar" },
|
||||
{ "shf", "application/shf+xml" },
|
||||
{ "sic", "application/vnd.wap.sic" },
|
||||
{ "sig", "application/pgp-signature" },
|
||||
{ "silo", "model/mesh" },
|
||||
{ "sis", "application/vnd.symbian.install" },
|
||||
{ "sisx", "application/vnd.symbian.install" },
|
||||
{ "sit", "application/x-stuffit" },
|
||||
{ "si", "text/vnd.wap.si" },
|
||||
{ "sitx", "application/x-stuffitx" },
|
||||
{ "skd", "application/vnd.koan" },
|
||||
{ "skm", "application/vnd.koan" },
|
||||
{ "skp", "application/vnd.koan" },
|
||||
{ "skt", "application/vnd.koan" },
|
||||
{ "slc", "application/vnd.wap.slc" },
|
||||
{ "sldm", "application/vnd.ms-powerpoint.slide.macroenabled.12" },
|
||||
{ "sldx", "application/vnd.openxmlformats-officedocument.presentationml.slide" },
|
||||
{ "slt", "application/vnd.epson.salt" },
|
||||
{ "sl", "text/vnd.wap.sl" },
|
||||
{ "smf", "application/vnd.stardivision.math" },
|
||||
{ "smi", "application/smil+xml" },
|
||||
{ "smil", "application/smil+xml" },
|
||||
{ "snd", "audio/basic" },
|
||||
{ "snf", "application/x-font-snf" },
|
||||
{ "so", "application/octet-stream" },
|
||||
{ "spc", "application/x-pkcs7-certificates" },
|
||||
{ "spf", "application/vnd.yamaha.smaf-phrase" },
|
||||
{ "spl", "application/x-futuresplash" },
|
||||
{ "spot", "text/vnd.in3d.spot" },
|
||||
{ "spp", "application/scvp-vp-response" },
|
||||
{ "spq", "application/scvp-vp-request" },
|
||||
{ "spx", "audio/ogg" },
|
||||
{ "src", "application/x-wais-source" },
|
||||
{ "srx", "application/sparql-results+xml" },
|
||||
{ "sse", "application/vnd.kodak-descriptor" },
|
||||
{ "ssf", "application/vnd.epson.ssf" },
|
||||
{ "ssml", "application/ssml+xml" },
|
||||
{ "sst", "application/vnd.ms-pkicertstore" },
|
||||
{ "stc", "application/vnd.sun.xml.calc.template" },
|
||||
{ "std", "application/vnd.sun.xml.draw.template" },
|
||||
{ "s", "text/x-asm" },
|
||||
{ "stf", "application/vnd.wt.stf" },
|
||||
{ "sti", "application/vnd.sun.xml.impress.template" },
|
||||
{ "stk", "application/hyperstudio" },
|
||||
{ "stl", "application/vnd.ms-pki.stl" },
|
||||
{ "stm", "text/html" },
|
||||
{ "str", "application/vnd.pg.format" },
|
||||
{ "stw", "application/vnd.sun.xml.writer.template" },
|
||||
{ "sus", "application/vnd.sus-calendar" },
|
||||
{ "susp", "application/vnd.sus-calendar" },
|
||||
{ "sv4cpio", "application/x-sv4cpio" },
|
||||
{ "sv4crc", "application/x-sv4crc" },
|
||||
{ "svd", "application/vnd.svd" },
|
||||
{ "svg", "image/svg+xml" },
|
||||
{ "svgz", "image/svg+xml" },
|
||||
{ "swa", "application/x-director" },
|
||||
{ "swf", "application/x-shockwave-flash" },
|
||||
{ "swi", "application/vnd.arastra.swi" },
|
||||
{ "sxc", "application/vnd.sun.xml.calc" },
|
||||
{ "sxd", "application/vnd.sun.xml.draw" },
|
||||
{ "sxg", "application/vnd.sun.xml.writer.global" },
|
||||
{ "sxi", "application/vnd.sun.xml.impress" },
|
||||
{ "sxm", "application/vnd.sun.xml.math" },
|
||||
{ "sxw", "application/vnd.sun.xml.writer" },
|
||||
{ "tao", "application/vnd.tao.intent-module-archive" },
|
||||
{ "t", "application/x-troff" },
|
||||
{ "tar", "application/x-tar" },
|
||||
{ "tcap", "application/vnd.3gpp2.tcap" },
|
||||
{ "tcl", "application/x-tcl" },
|
||||
{ "teacher", "application/vnd.smart.teacher" },
|
||||
{ "tex", "application/x-tex" },
|
||||
{ "texi", "application/x-texinfo" },
|
||||
{ "texinfo", "application/x-texinfo" },
|
||||
{ "text", "text/plain" },
|
||||
{ "tfm", "application/x-tex-tfm" },
|
||||
{ "tgz", "application/x-gzip" },
|
||||
{ "tiff", "image/tiff" },
|
||||
{ "tif", "image/tiff" },
|
||||
{ "tmo", "application/vnd.tmobile-livetv" },
|
||||
{ "torrent", "application/x-bittorrent" },
|
||||
{ "tpl", "application/vnd.groove-tool-template" },
|
||||
{ "tpt", "application/vnd.trid.tpt" },
|
||||
{ "tra", "application/vnd.trueapp" },
|
||||
{ "trm", "application/x-msterminal" },
|
||||
{ "tr", "text/troff" },
|
||||
{ "tsv", "text/tab-separated-values" },
|
||||
{ "ttc", "application/x-font-ttf" },
|
||||
{ "ttf", "application/x-font-ttf" },
|
||||
{ "twd", "application/vnd.simtech-mindmapper" },
|
||||
{ "twds", "application/vnd.simtech-mindmapper" },
|
||||
{ "txd", "application/vnd.genomatix.tuxedo" },
|
||||
{ "txf", "application/vnd.mobius.txf" },
|
||||
{ "txt", "text/plain" },
|
||||
{ "u32", "application/x-authorware-bin" },
|
||||
{ "udeb", "application/x-debian-package" },
|
||||
{ "ufd", "application/vnd.ufdl" },
|
||||
{ "ufdl", "application/vnd.ufdl" },
|
||||
{ "uls", "text/iuls" },
|
||||
{ "umj", "application/vnd.umajin" },
|
||||
{ "unityweb", "application/vnd.unity" },
|
||||
{ "uoml", "application/vnd.uoml+xml" },
|
||||
{ "uris", "text/uri-list" },
|
||||
{ "uri", "text/uri-list" },
|
||||
{ "urls", "text/uri-list" },
|
||||
{ "ustar", "application/x-ustar" },
|
||||
{ "utz", "application/vnd.uiq.theme" },
|
||||
{ "uu", "text/x-uuencode" },
|
||||
{ "vcd", "application/x-cdlink" },
|
||||
{ "vcf", "text/x-vcard" },
|
||||
{ "vcg", "application/vnd.groove-vcard" },
|
||||
{ "vcs", "text/x-vcalendar" },
|
||||
{ "vcx", "application/vnd.vcx" },
|
||||
{ "vis", "application/vnd.visionary" },
|
||||
{ "viv", "video/vnd.vivo" },
|
||||
{ "vor", "application/vnd.stardivision.writer" },
|
||||
{ "vox", "application/x-authorware-bin" },
|
||||
{ "vrml", "x-world/x-vrml" },
|
||||
{ "vsd", "application/vnd.visio" },
|
||||
{ "vsf", "application/vnd.vsf" },
|
||||
{ "vss", "application/vnd.visio" },
|
||||
{ "vst", "application/vnd.visio" },
|
||||
{ "vsw", "application/vnd.visio" },
|
||||
{ "vtu", "model/vnd.vtu" },
|
||||
{ "vxml", "application/voicexml+xml" },
|
||||
{ "w3d", "application/x-director" },
|
||||
{ "wad", "application/x-doom" },
|
||||
{ "wav", "audio/x-wav" },
|
||||
{ "wax", "audio/x-ms-wax" },
|
||||
{ "wbmp", "image/vnd.wap.wbmp" },
|
||||
{ "wbs", "application/vnd.criticaltools.wbs+xml" },
|
||||
{ "wbxml", "application/vnd.wap.wbxml" },
|
||||
{ "wcm", "application/vnd.ms-works" },
|
||||
{ "wdb", "application/vnd.ms-works" },
|
||||
{ "wiz", "application/msword" },
|
||||
{ "wks", "application/vnd.ms-works" },
|
||||
{ "wma", "audio/x-ms-wma" },
|
||||
{ "wmd", "application/x-ms-wmd" },
|
||||
{ "wmf", "application/x-msmetafile" },
|
||||
{ "wmlc", "application/vnd.wap.wmlc" },
|
||||
{ "wmlsc", "application/vnd.wap.wmlscriptc" },
|
||||
{ "wmls", "text/vnd.wap.wmlscript" },
|
||||
{ "wml", "text/vnd.wap.wml" },
|
||||
{ "wm", "video/x-ms-wm" },
|
||||
{ "wmv", "video/x-ms-wmv" },
|
||||
{ "wmx", "video/x-ms-wmx" },
|
||||
{ "wmz", "application/x-ms-wmz" },
|
||||
{ "wpd", "application/vnd.wordperfect" },
|
||||
{ "wpl", "application/vnd.ms-wpl" },
|
||||
{ "wps", "application/vnd.ms-works" },
|
||||
{ "wqd", "application/vnd.wqd" },
|
||||
{ "wri", "application/x-mswrite" },
|
||||
{ "wrl", "x-world/x-vrml" },
|
||||
{ "wrz", "x-world/x-vrml" },
|
||||
{ "wsdl", "application/wsdl+xml" },
|
||||
{ "wspolicy", "application/wspolicy+xml" },
|
||||
{ "wtb", "application/vnd.webturbo" },
|
||||
{ "wvx", "video/x-ms-wvx" },
|
||||
{ "x32", "application/x-authorware-bin" },
|
||||
{ "x3d", "application/vnd.hzn-3d-crossword" },
|
||||
{ "xaf", "x-world/x-vrml" },
|
||||
{ "xap", "application/x-silverlight-app" },
|
||||
{ "xar", "application/vnd.xara" },
|
||||
{ "xbap", "application/x-ms-xbap" },
|
||||
{ "xbd", "application/vnd.fujixerox.docuworks.binder" },
|
||||
{ "xbm", "image/x-xbitmap" },
|
||||
{ "xdm", "application/vnd.syncml.dm+xml" },
|
||||
{ "xdp", "application/vnd.adobe.xdp+xml" },
|
||||
{ "xdw", "application/vnd.fujixerox.docuworks" },
|
||||
{ "xenc", "application/xenc+xml" },
|
||||
{ "xer", "application/patch-ops-error+xml" },
|
||||
{ "xfdf", "application/vnd.adobe.xfdf" },
|
||||
{ "xfdl", "application/vnd.xfdl" },
|
||||
{ "xht", "application/xhtml+xml" },
|
||||
{ "xhtml", "application/xhtml+xml" },
|
||||
{ "xhvml", "application/xv+xml" },
|
||||
{ "xif", "image/vnd.xiff" },
|
||||
{ "xla", "application/vnd.ms-excel" },
|
||||
{ "xlam", "application/vnd.ms-excel.addin.macroenabled.12" },
|
||||
{ "xlb", "application/vnd.ms-excel" },
|
||||
{ "xlc", "application/vnd.ms-excel" },
|
||||
{ "xlm", "application/vnd.ms-excel" },
|
||||
{ "xls", "application/vnd.ms-excel" },
|
||||
{ "xlsb", "application/vnd.ms-excel.sheet.binary.macroenabled.12" },
|
||||
{ "xlsm", "application/vnd.ms-excel.sheet.macroenabled.12" },
|
||||
{ "xlsx", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" },
|
||||
{ "xlt", "application/vnd.ms-excel" },
|
||||
{ "xltm", "application/vnd.ms-excel.template.macroenabled.12" },
|
||||
{ "xltx", "application/vnd.openxmlformats-officedocument.spreadsheetml.template" },
|
||||
{ "xlw", "application/vnd.ms-excel" },
|
||||
{ "xml", "application/xml" },
|
||||
{ "xo", "application/vnd.olpc-sugar" },
|
||||
{ "xof", "x-world/x-vrml" },
|
||||
{ "xop", "application/xop+xml" },
|
||||
{ "xpdl", "application/xml" },
|
||||
{ "xpi", "application/x-xpinstall" },
|
||||
{ "xpm", "image/x-xpixmap" },
|
||||
{ "xpr", "application/vnd.is-xpr" },
|
||||
{ "xps", "application/vnd.ms-xpsdocument" },
|
||||
{ "xpw", "application/vnd.intercon.formnet" },
|
||||
{ "xpx", "application/vnd.intercon.formnet" },
|
||||
{ "xsl", "application/xml" },
|
||||
{ "xslt", "application/xslt+xml" },
|
||||
{ "xsm", "application/vnd.syncml+xml" },
|
||||
{ "xspf", "application/xspf+xml" },
|
||||
{ "xul", "application/vnd.mozilla.xul+xml" },
|
||||
{ "xvm", "application/xv+xml" },
|
||||
{ "xvml", "application/xv+xml" },
|
||||
{ "xwd", "image/x-xwindowdump" },
|
||||
{ "xyz", "chemical/x-xyz" },
|
||||
{ "z", "application/x-compress" },
|
||||
{ "zaz", "application/vnd.zzazz.deck+xml" },
|
||||
{ "zip", "application/zip" },
|
||||
{ "zir", "application/vnd.zul" },
|
||||
{ "zirz", "application/vnd.zul" },
|
||||
{ "zmm", "application/vnd.handheld-entertainment+xml" }
|
||||
};
|
||||
|
||||
public static boolean isDefaultMimeType(String mimeType) {
|
||||
return isSameMimeType(mimeType, DEFAULT_ATTACHMENT_MIME_TYPE);
|
||||
}
|
||||
|
||||
public static String getMimeTypeByExtension(String filename) {
|
||||
String returnedType = null;
|
||||
String extension = null;
|
||||
|
||||
if (filename != null && filename.lastIndexOf('.') != -1) {
|
||||
extension = filename.substring(filename.lastIndexOf('.') + 1).toLowerCase(Locale.US);
|
||||
returnedType = android.webkit.MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension);
|
||||
}
|
||||
// If the MIME type set by the user's mailer is application/octet-stream, try to figure
|
||||
// out whether there's a sane file type extension.
|
||||
if (returnedType != null && !isSameMimeType(returnedType, DEFAULT_ATTACHMENT_MIME_TYPE)) {
|
||||
return returnedType;
|
||||
} else if (extension != null) {
|
||||
for (String[] contentTypeMapEntry : MIME_TYPE_BY_EXTENSION_MAP) {
|
||||
if (contentTypeMapEntry[0].equals(extension)) {
|
||||
return contentTypeMapEntry[1];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return DEFAULT_ATTACHMENT_MIME_TYPE;
|
||||
}
|
||||
|
||||
public static String getExtensionByMimeType(@NotNull String mimeType) {
|
||||
String lowerCaseMimeType = mimeType.toLowerCase(Locale.US);
|
||||
for (String[] contentTypeMapEntry : MIME_TYPE_BY_EXTENSION_MAP) {
|
||||
if (contentTypeMapEntry[1].equals(lowerCaseMimeType)) {
|
||||
return contentTypeMapEntry[0];
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public static boolean isSupportedImageType(String mimeType) {
|
||||
return isSameMimeType(mimeType, "image/jpeg") || isSameMimeType(mimeType, "image/png") ||
|
||||
isSameMimeType(mimeType, "image/gif") || isSameMimeType(mimeType, "image/webp");
|
||||
}
|
||||
|
||||
public static boolean isSupportedImageExtension(String filename) {
|
||||
String mimeType = getMimeTypeByExtension(filename);
|
||||
return isSupportedImageType(mimeType);
|
||||
}
|
||||
|
||||
public static boolean isSameMimeType(String mimeType, String otherMimeType) {
|
||||
return mimeType != null && mimeType.equalsIgnoreCase(otherMimeType);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
package com.fsck.k9.helper
|
||||
|
||||
class MutableBoolean(var value: Boolean)
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
package com.fsck.k9.helper
|
||||
|
||||
import java.util.concurrent.ThreadFactory
|
||||
|
||||
class NamedThreadFactory(private val threadNamePrefix: String) : ThreadFactory {
|
||||
var counter: Int = 0
|
||||
|
||||
override fun newThread(runnable: Runnable) = Thread(runnable, "$threadNamePrefix-${ counter++ }")
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
package com.fsck.k9.helper;
|
||||
|
||||
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
|
||||
|
||||
// Source: http://stackoverflow.com/a/18000094
|
||||
public class ParcelableUtil {
|
||||
public static byte[] marshall(Parcelable parceable) {
|
||||
Parcel parcel = Parcel.obtain();
|
||||
parceable.writeToParcel(parcel, 0);
|
||||
byte[] bytes = parcel.marshall();
|
||||
parcel.recycle();
|
||||
return bytes;
|
||||
}
|
||||
|
||||
public static <T> T unmarshall(byte[] bytes, Parcelable.Creator<T> creator) {
|
||||
Parcel parcel = unmarshall(bytes);
|
||||
T result = creator.createFromParcel(parcel);
|
||||
parcel.recycle();
|
||||
return result;
|
||||
}
|
||||
|
||||
private static Parcel unmarshall(byte[] bytes) {
|
||||
Parcel parcel = Parcel.obtain();
|
||||
parcel.unmarshall(bytes, 0, bytes.length);
|
||||
parcel.setDataPosition(0);
|
||||
return parcel;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
package com.fsck.k9.helper
|
||||
|
||||
import android.app.PendingIntent
|
||||
import android.os.Build
|
||||
|
||||
object PendingIntentCompat {
|
||||
@JvmField
|
||||
val FLAG_IMMUTABLE = if (Build.VERSION.SDK_INT >= 23) PendingIntent.FLAG_IMMUTABLE else 0
|
||||
|
||||
@JvmField
|
||||
val FLAG_MUTABLE = if (Build.VERSION.SDK_INT >= 31) PendingIntent.FLAG_MUTABLE else 0
|
||||
}
|
||||
90
app/core/src/main/java/com/fsck/k9/helper/ReplyToParser.java
Normal file
90
app/core/src/main/java/com/fsck/k9/helper/ReplyToParser.java
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
package com.fsck.k9.helper;
|
||||
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
|
||||
import com.fsck.k9.Account;
|
||||
import com.fsck.k9.mail.Address;
|
||||
import com.fsck.k9.mail.Message;
|
||||
import com.fsck.k9.mail.Message.RecipientType;
|
||||
|
||||
|
||||
public class ReplyToParser {
|
||||
|
||||
public ReplyToAddresses getRecipientsToReplyTo(Message message, Account account) {
|
||||
Address[] candidateAddress;
|
||||
|
||||
Address[] replyToAddresses = message.getReplyTo();
|
||||
Address[] listPostAddresses = ListHeaders.getListPostAddresses(message);
|
||||
Address[] fromAddresses = message.getFrom();
|
||||
|
||||
if (replyToAddresses.length > 0) {
|
||||
candidateAddress = replyToAddresses;
|
||||
} else if (listPostAddresses.length > 0) {
|
||||
candidateAddress = listPostAddresses;
|
||||
} else {
|
||||
candidateAddress = fromAddresses;
|
||||
}
|
||||
|
||||
boolean replyToAddressIsUserIdentity = account.isAnIdentity(candidateAddress);
|
||||
if (replyToAddressIsUserIdentity) {
|
||||
candidateAddress = message.getRecipients(RecipientType.TO);
|
||||
}
|
||||
|
||||
return new ReplyToAddresses(candidateAddress);
|
||||
}
|
||||
|
||||
public ReplyToAddresses getRecipientsToReplyAllTo(Message message, Account account) {
|
||||
List<Address> replyToAddresses = Arrays.asList(getRecipientsToReplyTo(message, account).to);
|
||||
|
||||
HashSet<Address> alreadyAddedAddresses = new HashSet<>(replyToAddresses);
|
||||
ArrayList<Address> toAddresses = new ArrayList<>(replyToAddresses);
|
||||
ArrayList<Address> ccAddresses = new ArrayList<>();
|
||||
|
||||
for (Address address : message.getFrom()) {
|
||||
if (!alreadyAddedAddresses.contains(address) && !account.isAnIdentity(address)) {
|
||||
toAddresses.add(address);
|
||||
alreadyAddedAddresses.add(address);
|
||||
}
|
||||
}
|
||||
|
||||
for (Address address : message.getRecipients(RecipientType.TO)) {
|
||||
if (!alreadyAddedAddresses.contains(address) && !account.isAnIdentity(address)) {
|
||||
toAddresses.add(address);
|
||||
alreadyAddedAddresses.add(address);
|
||||
}
|
||||
}
|
||||
|
||||
for (Address address : message.getRecipients(RecipientType.CC)) {
|
||||
if (!alreadyAddedAddresses.contains(address) && !account.isAnIdentity(address)) {
|
||||
ccAddresses.add(address);
|
||||
alreadyAddedAddresses.add(address);
|
||||
}
|
||||
}
|
||||
|
||||
return new ReplyToAddresses(toAddresses, ccAddresses);
|
||||
}
|
||||
|
||||
public static class ReplyToAddresses {
|
||||
public final Address[] to;
|
||||
public final Address[] cc;
|
||||
|
||||
@VisibleForTesting
|
||||
public ReplyToAddresses(List<Address> toAddresses, List<Address> ccAddresses) {
|
||||
to = toAddresses.toArray(new Address[toAddresses.size()]);
|
||||
cc = ccAddresses.toArray(new Address[ccAddresses.size()]);
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
public ReplyToAddresses(Address[] toAddresses) {
|
||||
to = toAddresses;
|
||||
cc = new Address[0];
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,63 @@
|
|||
package com.fsck.k9.helper;
|
||||
|
||||
|
||||
import android.os.Bundle;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
|
||||
|
||||
public class RetainFragment<T> extends Fragment {
|
||||
private T data;
|
||||
private boolean cleared;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
setRetainInstance(true);
|
||||
}
|
||||
|
||||
public T getData() {
|
||||
return data;
|
||||
}
|
||||
|
||||
public boolean hasData() {
|
||||
return data != null;
|
||||
}
|
||||
|
||||
public void setData(T data) {
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
public static <T> RetainFragment<T> findOrNull(FragmentManager fm, String tag) {
|
||||
// noinspection unchecked, we know this is the the right type
|
||||
return (RetainFragment<T>) fm.findFragmentByTag(tag);
|
||||
}
|
||||
|
||||
public static <T> RetainFragment<T> findOrCreate(FragmentManager fm, String tag) {
|
||||
// noinspection unchecked, we know this is the the right type
|
||||
RetainFragment<T> retainFragment = (RetainFragment<T>) fm.findFragmentByTag(tag);
|
||||
|
||||
if (retainFragment == null || retainFragment.cleared) {
|
||||
retainFragment = new RetainFragment<>();
|
||||
fm.beginTransaction()
|
||||
.add(retainFragment, tag)
|
||||
.commitAllowingStateLoss();
|
||||
}
|
||||
|
||||
return retainFragment;
|
||||
}
|
||||
|
||||
public void clearAndRemove(FragmentManager fm) {
|
||||
data = null;
|
||||
cleared = true;
|
||||
|
||||
if (fm.isDestroyed()) {
|
||||
return;
|
||||
}
|
||||
|
||||
fm.beginTransaction()
|
||||
.remove(this)
|
||||
.commitAllowingStateLoss();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
package com.fsck.k9.helper;
|
||||
|
||||
import android.text.Editable;
|
||||
import android.text.TextWatcher;
|
||||
|
||||
/**
|
||||
* all methods empty - but this way we can have TextWatchers with less boiler-plate where
|
||||
* we just override the methods we want and not always all 3
|
||||
*/
|
||||
public class SimpleTextWatcher implements TextWatcher {
|
||||
@Override
|
||||
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTextChanged(CharSequence s, int start, int before, int count) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterTextChanged(Editable s) {
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,76 @@
|
|||
/*
|
||||
* Copyright 2017 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.fsck.k9.helper;
|
||||
|
||||
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
import androidx.lifecycle.LifecycleOwner;
|
||||
import androidx.lifecycle.MutableLiveData;
|
||||
import androidx.lifecycle.Observer;
|
||||
import androidx.annotation.MainThread;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import timber.log.Timber;
|
||||
|
||||
|
||||
/**
|
||||
* A lifecycle-aware observable that sends only new updates after subscription, used for events like
|
||||
* navigation and Snackbar messages.
|
||||
* <p>
|
||||
* This avoids a common problem with events: on configuration change (like rotation) an update
|
||||
* can be emitted if the observer is active. This LiveData only calls the observable if there's an
|
||||
* explicit call to setValue() or call().
|
||||
* <p>
|
||||
* Note that only one observer is going to be notified of changes.
|
||||
*/
|
||||
public class SingleLiveEvent<T> extends MutableLiveData<T> {
|
||||
private final AtomicBoolean pending = new AtomicBoolean(false);
|
||||
|
||||
@MainThread
|
||||
public void observe(@NonNull LifecycleOwner owner, @NonNull final Observer<? super T> observer) {
|
||||
|
||||
if (hasActiveObservers()) {
|
||||
Timber.w("Multiple observers registered but only one will be notified of changes.");
|
||||
}
|
||||
|
||||
// Observe the internal MutableLiveData
|
||||
super.observe(owner, new Observer<T>() {
|
||||
@Override
|
||||
public void onChanged(@Nullable T t) {
|
||||
if (pending.compareAndSet(true, false)) {
|
||||
observer.onChanged(t);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@MainThread
|
||||
public void setValue(@Nullable T t) {
|
||||
pending.set(true);
|
||||
super.setValue(t);
|
||||
}
|
||||
|
||||
/**
|
||||
* Used for cases where T is Void, to make calls cleaner.
|
||||
*/
|
||||
@MainThread
|
||||
public void recall() {
|
||||
setValue(getValue());
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
@file:JvmName("StringHelper")
|
||||
|
||||
package com.fsck.k9.helper
|
||||
|
||||
fun isNullOrEmpty(text: String?) = text.isNullOrEmpty()
|
||||
22
app/core/src/main/java/com/fsck/k9/helper/Timing.kt
Normal file
22
app/core/src/main/java/com/fsck/k9/helper/Timing.kt
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
package com.fsck.k9.helper
|
||||
|
||||
import android.os.SystemClock
|
||||
|
||||
/**
|
||||
* Executes the given [block] and returns elapsed realtime in milliseconds.
|
||||
*/
|
||||
inline fun measureRealtimeMillis(block: () -> Unit): Long {
|
||||
val start = SystemClock.elapsedRealtime()
|
||||
block()
|
||||
return SystemClock.elapsedRealtime() - start
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes the given [block] and returns pair of elapsed realtime in milliseconds and result of the code block.
|
||||
*/
|
||||
inline fun <T> measureRealtimeMillisWithResult(block: () -> T): Pair<Long, T> {
|
||||
val start = SystemClock.elapsedRealtime()
|
||||
val result = block()
|
||||
val elapsedTime = SystemClock.elapsedRealtime() - start
|
||||
return elapsedTime to result
|
||||
}
|
||||
10
app/core/src/main/java/com/fsck/k9/helper/UnsubscribeUri.kt
Normal file
10
app/core/src/main/java/com/fsck/k9/helper/UnsubscribeUri.kt
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
package com.fsck.k9.helper
|
||||
|
||||
import android.net.Uri
|
||||
|
||||
sealed interface UnsubscribeUri {
|
||||
val uri: Uri
|
||||
}
|
||||
|
||||
data class MailtoUnsubscribeUri(override val uri: Uri) : UnsubscribeUri
|
||||
data class HttpsUnsubscribeUri(override val uri: Uri) : UnsubscribeUri
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue