Repo created

This commit is contained in:
Fr4nz D13trich 2025-11-24 18:55:42 +01:00
parent a629de6271
commit 3cef7c5092
2161 changed files with 246605 additions and 2 deletions

15
.editorconfig Normal file
View file

@ -0,0 +1,15 @@
root = true
[*]
charset = utf-8
indent_size = 4
indent_style = space
insert_final_newline = true
[*.{kt,kts}]
ij_kotlin_imports_layout = *,^
ij_kotlin_allow_trailing_comma = true
ij_kotlin_allow_trailing_comma_on_call_site = true
[*.{yml,yaml,json,toml}]
indent_size = 2

4
.gitattributes vendored Normal file
View file

@ -0,0 +1,4 @@
* text=auto eol=lf
*.bat text eol=crlf
*.jar binary

32
.gitignore vendored Normal file
View file

@ -0,0 +1,32 @@
# Local per-repo rules can be added to the .git/info/exclude file in your
# repo. These rules are not committed with the repo so they are not shared
# with others. This method can be used for locally-generated files that you
# dont expect other users to generate, like files created by your editor.
.DS_Store
.settings
.classpath
bin
captures
coverage
coverage.ec
coverage.em
gen
javadoc
junit-report.xml
lint-results.*ml
lint-results_files
local.properties
monkey.txt
*~
*.iws
atlassian-ide-plugin.xml
target
build
.gradle
out
build.xml
proguard-project.txt
.idea/
*.iml
user-manual/screenshots
user-manual/output

8
.tx/config Normal file
View file

@ -0,0 +1,8 @@
[main]
host = https://www.transifex.com
lang_map = de_LI: de-rLI, es_GT: es-rGT, fa_IR: fa-rIR, fo_FO: fo-rFO, am_ET: am-rET, ar_DZ: ar-rDZ, ca_ES: ca-rES, ii_CN: ii-rCN, mr_IN: mr-rIN, ms_BN: ms-rBN, zh_MO: zh-rMO, ba_RU: ba-rRU, es_MX: es-rMX, es_PE: es-rPE, tk_TM: tk-rTM, es_PR: es-rPR, pt_BR: pt-rBR, smn_FI: smn-rFI, xh_ZA: xh-rZA, zu_ZA: zu-rZA, ar_EG: ar-rEG, si_LK: si-rLK, tzm_DZ: tzm-rDZ, es_ES: es-rES, ne_NP: ne-rNP, qut_GT: qut-rGT, th_TH: th-rTH, he_IL: iw-rIL, cy_GB: cy-rGB, es_AR: es-rAR, es_EC: es-rEC, fr_FR: fr-rFR, gu_IN: gu-rIN, ja_JP: ja-rJP, pl_PL: pl-rPL, sr_CS: sr-rCS, ar_YE: ar-rYE, dsb_DE: dsb-rDE, en_ZA: en-rZA, se_FI: se-rFI, tt_RU: tt-rRU, uk_UA: uk-rUA, zh_CN: zh-rCN, ar_OM: ar-rOM, et_EE: et-rEE, ky_KG: ky-rKG, el_GR: el-rGR, es_PY: es-rPY, hi_IN: hi-rIN, sma_SE: sma-rSE, tn_ZA: tn-rZA, ar_IQ: ar-rIQ, en_TT: en-rTT, se_NO: se-rNO, kk_KZ: kk-rKZ, mn_CN: mn-rCN, mt_MT: mt-rMT, nso_ZA: nso-rZA, ro_RO: ro-rRO, ar_LB: ar-rLB, es_VE: es-rVE, ka_GE: ka-rGE, nl_BE: nl-rBE, pa_IN: pa-rIN, es_SV: es-rSV, it_CH: it-rCH, lb_LU: lb-rLU, arn_CL: arn-rCL, bo_CN: bo-rCN, en_GB: en-rGB, fi_FI: fi-rFI, mi_NZ: mi-rNZ, ar_BH: ar-rBH, ar_MA: ar-rMA, ar_SY: ar-rSY, vi_VN: vi-rVN, fr_CH: fr-rCH, ko_KR: ko-rKR, quz_PE: quz-rPE, af_ZA: af-rZA, dv_MV: dv-rMV, en_JM: en-rJM, sr_ME: sr-rME, sv_SE: sv-rSE, ur_PK: ur-rPK, zh_SG: zh-rSG, ar_QA: ar-rQA, nb_NO: nb-rNO, sk_SK: sk-rSK, hr_HR: hr-rHR, kok_IN: kok-rIN, ms_MY: ms-rMY, nl_NL: nl-rNL, te_IN: te-rIN, en_US: en-rUS, en_ZW: en-rZW, fr_LU: fr-rLU, syr_SY: syr-rSY, he: iw, en_NZ: en-rNZ, fr_MC: fr-rMC, ru_RU: ru-rRU, es_PA: es-rPA, es_UY: es-rUY, se_SE: se-rSE, as_IN: as-rIN, de_LU: de-rLU, es_DO: es-rDO, sms_FI: sms-rFI, quz_BO: quz-rBO, smj_NO: smj-rNO, sr_BA: sr-rBA, sv_FI: sv-rFI, ar_TN: ar-rTN, en_CA: en-rCA, ig_NG: ig-rNG, de_DE: de-rDE, es_NI: es-rNI, it_IT: it-rIT, pt_PT: pt-rPT, sah_RU: sah-rRU, ar_AE: ar-rAE, da_DK: da-rDK, de_AT: de-rAT, sq_AL: sq-rAL, sr_RS: sr-rRS, no_NO: no-rNO, ar_KW: ar-rKW, nn_NO: nn-rNO, sma_NO: sma-rNO, gsw_FR: gsw-rFR, hr_BA: hr-rBA, prs_AF: prs-rAF, hu_HU: hu-rHU, id_ID: id-rID, is_IS: is-rIS, kl_GL: kl-rGL, lt_LT: lt-rLT, en_AU: en-rAU, en_BZ: en-rBZ, en_PH: en-rPH, rm_CH: rm-rCH, cs_CZ: cs-rCZ, es_CL: es-rCL, fil_PH: fil-rPH, fr_BE: fr-rBE, ga_IE: ga-rIE, ar_LY: ar-rLY, bn_IN: bn-rIN, co_FR: co-rFR, gd_GB: gd-rGB, or_IN: or-rIN, ta_IN: ta-rIN, yo_NG: yo-rNG, en_MY: en-rMY, lv_LV: lv-rLV, sa_IN: sa-rIN, km_KH: km-rKH, mk_MK: mk-rMK, ps_AF: ps-rAF, rw_RW: rw-rRW, uz_UZ: uz-rUZ, ar_SA: ar-rSA, be_BY: be-rBY, es_CO: es-rCO, hsb_DE: hsb-rDE, de_CH: de-rCH, es_CR: es-rCR, eu_ES: eu-rES, ug_CN: ug-rCN, es_BO: es-rBO, gl_ES: gl-rES, sl_SI: sl-rSI, es_US: es-rUS, fy_NL: fy-rNL, kn_IN: kn-rIN, oc_FR: oc-rFR, tg_TJ: tg-rTJ, bg_BG: bg-rBG, bn_BD: bn-rBD, bs_BA: bs-rBA, tr_TR: tr-rTR, wo_SN: wo-rSN, az_AZ: az-rAZ, en_SG: en-rSG, moh_CA: moh-rCA, fr_CA: fr-rCA, ha_NG: ha-rNG, lo_LA: lo-rLA, mn_MN: mn-rMN, quz_EC: quz-rEC, en_IE: en-rIE, en_IN: en-rIN, es_HN: es-rHN, sw_KE: sw-rKE, id: in, iu_CA: iu-rCA, ml_IN: ml-rIN, smj_SE: smj-rSE, zh_HK: zh-rHK, zh_TW: zh-rTW, ar_JO: ar-rJO, br_FR: br-rFR, hy_AM: hy-rAM
[o:k-9:p:k9mail:r:strings]
file_filter = app/ui/legacy/src/main/res/values-<lang>/strings.xml
source_file = app/ui/legacy/src/main/res/values/strings.xml
source_lang = en

202
LICENSE Normal file
View file

@ -0,0 +1,202 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
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.

3
NOTICE Normal file
View file

@ -0,0 +1,3 @@
K-9 Mail
Copyright 2008-2016, K-9 Mail Developers
Copyright 2005-2016, The Android Open Source Project

View file

@ -1,3 +1,68 @@
# mail
# K-9 Mail
Monocles E-Mail Client for Android
[![Latest release](https://img.shields.io/github/release/thundernest/k-9.svg?style=flat-square)](https://github.com/thundernest/k-9/releases/latest)
[![Latest beta release](https://img.shields.io/github/v/release/thundernest/k-9.svg?include_prereleases&style=flat-square)](https://github.com/thundernest/k-9/releases)
K-9 Mail is an open-source email client for Android.
## Download
K-9 Mail can be downloaded from a couple of sources:
- [Google Play](https://play.google.com/store/apps/details?id=com.fsck.k9)
- [F-Droid](https://f-droid.org/repository/browse/?fdid=com.fsck.k9)
- [Github Releases](https://github.com/thundernest/k-9/releases)
You might also be interested in becoming a [tester](https://forum.k9mail.app/t/how-do-i-become-a-beta-tester/68) to get an early look at new versions.
## Release Notes
Check out the [Release Notes](https://github.com/thundernest/k-9/wiki/ReleaseNotes) to find out what changed
in each version of K-9 Mail.
## Need Help?
If the app is not behaving like it should, you might find these resources helpful:
- [User Manual](https://docs.k9mail.app/)
- [Frequently Asked Questions](https://forum.k9mail.app/c/faq)
- [Support Forum](https://forum.k9mail.app/)
## Translations
Interested in helping to translate K-9 Mail? Contribute here:
https://www.transifex.com/projects/p/k9mail/
## Contributing
Thank you for contributing! If you're unfamiliar with the code, start by reading the [developer documentation](docs/DESIGN.md)
Please fork this repository and contribute back using [pull requests](https://github.com/thundernest/k-9/pulls).
Any contributions, large or small, major features, bug fixes, unit/integration tests are welcomed and appreciated
but will be thoroughly reviewed and discussed.
Please make sure you read the [Code Style Guidelines](https://github.com/thundernest/k-9/wiki/CodeStyle).
## Communication
Aside from discussing changes in [pull requests](https://github.com/thundernest/k-9/pulls) and
[issues](https://github.com/thundernest/k-9/issues) we use the following communication services:
- Matrix: [#k9mail:matrix.org](https://matrix.to/#/#tb-android:mozilla.org)
- IRC: [#k9mail on Libera Chat](https://web.libera.chat/#k9mail)
- [Developer mailing list](https://groups.google.com/forum/#!forum/k-9-dev)
## License
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.

7
app-ui-catalog/README.md Normal file
View file

@ -0,0 +1,7 @@
# Thunderbird UI Catalog
Uses [`:core:ui:compose:designsystem`](../core/ui/compose/designsystem/README.md)
This is a catalog of all the components in the Thunderbird design system.
It is a work in progress, and will be updated as the design system evolves.

View file

@ -0,0 +1,21 @@
plugins {
id(ThunderbirdPlugins.App.androidCompose)
}
android {
namespace = "app.k9mail.ui.catalog"
defaultConfig {
applicationId = "app.k9mail.ui.catalog"
versionCode = 1
versionName = "1.0"
}
}
dependencies {
implementation(projects.core.ui.compose.designsystem)
implementation(libs.androidx.compose.material)
androidTestImplementation(libs.androidx.test.ext.junit.ktx)
androidTestImplementation(libs.androidx.test.espresso.core)
}

21
app-ui-catalog/proguard-rules.pro vendored Normal file
View file

@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile

View file

@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:roundIcon="@mipmap/ic_launcher_round"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/Theme.Thunderbird"
>
<activity
android:name=".CatalogActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

View file

@ -0,0 +1,18 @@
package app.k9mail.ui.catalog
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.core.view.WindowCompat
class CatalogActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
WindowCompat.setDecorFitsSystemWindows(window, false)
setContent {
CatalogScreen()
}
}
}

View file

@ -0,0 +1,89 @@
package app.k9mail.ui.catalog
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import app.k9mail.core.ui.compose.common.DevicePreviews
import app.k9mail.core.ui.compose.designsystem.atom.Surface
import app.k9mail.core.ui.compose.designsystem.template.ResponsiveContent
import app.k9mail.core.ui.compose.theme.K9Theme
import app.k9mail.core.ui.compose.theme.MainTheme
import app.k9mail.core.ui.compose.theme.ThunderbirdTheme
import app.k9mail.ui.catalog.items.buttonItems
import app.k9mail.ui.catalog.items.colorItems
import app.k9mail.ui.catalog.items.imageItems
import app.k9mail.ui.catalog.items.selectionControlItems
import app.k9mail.ui.catalog.items.textFieldItems
import app.k9mail.ui.catalog.items.themeHeaderItem
import app.k9mail.ui.catalog.items.themeSelectorItems
import app.k9mail.ui.catalog.items.typographyItems
@Composable
fun CatalogContent(
catalogTheme: CatalogTheme,
catalogThemeVariant: CatalogThemeVariant,
onThemeChange: () -> Unit,
onThemeVariantChange: () -> Unit,
contentPadding: PaddingValues,
modifier: Modifier = Modifier,
) {
Surface {
ResponsiveContent {
LazyVerticalGrid(
columns = GridCells.Adaptive(300.dp),
contentPadding = contentPadding,
horizontalArrangement = Arrangement.spacedBy(MainTheme.spacings.double),
verticalArrangement = Arrangement.spacedBy(MainTheme.spacings.double),
modifier = modifier.padding(MainTheme.spacings.double),
) {
themeHeaderItem(text = "Thunderbird Catalog")
themeSelectorItems(
catalogTheme = catalogTheme,
catalogThemeVariant = catalogThemeVariant,
onThemeChange = onThemeChange,
onThemeVariantChange = onThemeVariantChange,
)
typographyItems()
colorItems()
buttonItems()
selectionControlItems()
textFieldItems()
imageItems()
}
}
}
}
@DevicePreviews
@Composable
internal fun CatalogContentK9ThemePreview() {
K9Theme {
CatalogContent(
catalogTheme = CatalogTheme.K9,
catalogThemeVariant = CatalogThemeVariant.LIGHT,
onThemeChange = {},
onThemeVariantChange = {},
contentPadding = PaddingValues(),
)
}
}
@DevicePreviews
@Composable
internal fun CatalogContentThunderbirdThemePreview() {
ThunderbirdTheme {
CatalogContent(
catalogTheme = CatalogTheme.THUNDERBIRD,
catalogThemeVariant = CatalogThemeVariant.LIGHT,
onThemeChange = {},
onThemeVariantChange = {},
contentPadding = PaddingValues(),
)
}
}

View file

@ -0,0 +1,50 @@
package app.k9mail.ui.catalog
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.asPaddingValues
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.systemBars
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import app.k9mail.core.ui.compose.common.DevicePreviews
@Composable
fun CatalogScreen(
modifier: Modifier = Modifier,
) {
val themeState = remember { mutableStateOf(CatalogTheme.K9) }
val themeVariantState = remember { mutableStateOf(CatalogThemeVariant.LIGHT) }
CatalogThemeSwitch(theme = themeState.value, themeVariation = themeVariantState.value) {
val contentPadding = WindowInsets.systemBars.asPaddingValues()
CatalogContent(
catalogTheme = themeState.value,
catalogThemeVariant = themeVariantState.value,
onThemeChange = {
themeState.value = when (themeState.value) {
CatalogTheme.K9 -> CatalogTheme.THUNDERBIRD
CatalogTheme.THUNDERBIRD -> CatalogTheme.K9
}
},
onThemeVariantChange = {
themeVariantState.value = when (themeVariantState.value) {
CatalogThemeVariant.LIGHT -> CatalogThemeVariant.DARK
CatalogThemeVariant.DARK -> CatalogThemeVariant.LIGHT
}
},
contentPadding = contentPadding,
modifier = Modifier
.fillMaxSize()
.then(modifier),
)
}
}
@DevicePreviews
@Composable
internal fun CatalogScreenPreview() {
CatalogScreen()
}

View file

@ -0,0 +1,13 @@
package app.k9mail.ui.catalog
enum class CatalogTheme(
private val displayName: String,
) {
K9("K-9"),
THUNDERBIRD("Thunderbird"),
;
override fun toString(): String {
return displayName
}
}

View file

@ -0,0 +1,29 @@
package app.k9mail.ui.catalog
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import app.k9mail.core.ui.compose.designsystem.atom.button.Button
import app.k9mail.core.ui.compose.designsystem.atom.text.TextBody1
import app.k9mail.core.ui.compose.theme.MainTheme
@Composable
fun CatalogThemeSelector(
catalogTheme: CatalogTheme,
modifier: Modifier = Modifier,
onThemeChangeClick: () -> Unit,
) {
Row(
modifier = modifier,
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(MainTheme.spacings.default),
) {
TextBody1(text = "Change theme:")
Button(
text = catalogTheme.toString(),
onClick = onThemeChangeClick,
)
}
}

View file

@ -0,0 +1,26 @@
package app.k9mail.ui.catalog
import androidx.compose.runtime.Composable
import app.k9mail.core.ui.compose.theme.K9Theme
import app.k9mail.core.ui.compose.theme.ThunderbirdTheme
@Composable
fun CatalogThemeSwitch(
theme: CatalogTheme,
themeVariation: CatalogThemeVariant,
content: @Composable () -> Unit,
) {
when (theme) {
CatalogTheme.K9 -> K9Theme(
darkTheme = isDarkVariation(themeVariation),
content = content,
)
CatalogTheme.THUNDERBIRD -> ThunderbirdTheme(
darkTheme = isDarkVariation(themeVariation),
content = content,
)
}
}
private fun isDarkVariation(themeVariation: CatalogThemeVariant): Boolean =
themeVariation == CatalogThemeVariant.DARK

View file

@ -0,0 +1,5 @@
package app.k9mail.ui.catalog
enum class CatalogThemeVariant {
LIGHT, DARK
}

View file

@ -0,0 +1,29 @@
package app.k9mail.ui.catalog
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import app.k9mail.core.ui.compose.designsystem.atom.Checkbox
import app.k9mail.core.ui.compose.designsystem.atom.text.TextBody1
import app.k9mail.core.ui.compose.theme.MainTheme
@Composable
fun CatalogThemeVariantSelector(
catalogThemeVariant: CatalogThemeVariant,
modifier: Modifier = Modifier,
onThemeVariantChange: () -> Unit,
) {
Row(
modifier = modifier,
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(MainTheme.spacings.default),
) {
TextBody1(text = "Set dark mode:")
Checkbox(
checked = catalogThemeVariant == CatalogThemeVariant.DARK,
onCheckedChange = { onThemeVariantChange() },
)
}
}

View file

@ -0,0 +1,40 @@
package app.k9mail.ui.catalog.items
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.lazy.grid.LazyGridScope
import app.k9mail.core.ui.compose.designsystem.atom.button.Button
import app.k9mail.core.ui.compose.designsystem.atom.button.ButtonOutlined
import app.k9mail.core.ui.compose.designsystem.atom.button.ButtonText
import app.k9mail.core.ui.compose.theme.MainTheme
fun LazyGridScope.buttonItems() {
sectionHeaderItem(text = "Buttons")
sectionSubtitleItem(text = "Contained")
item {
Row(
horizontalArrangement = Arrangement.spacedBy(MainTheme.spacings.default),
) {
Button(text = "Enabled", onClick = { })
Button(text = "Disabled", onClick = { }, enabled = false)
}
}
sectionSubtitleItem(text = "Outlined")
item {
Row(
horizontalArrangement = Arrangement.spacedBy(MainTheme.spacings.default),
) {
ButtonOutlined(text = "Enabled", onClick = { })
ButtonOutlined(text = "Disabled", onClick = { }, enabled = false)
}
}
sectionSubtitleItem(text = "Text")
item {
Row(
horizontalArrangement = Arrangement.spacedBy(MainTheme.spacings.default),
) {
ButtonText(text = "Enabled", onClick = { })
ButtonText(text = "Disabled", onClick = { }, enabled = false)
}
}
}

View file

@ -0,0 +1,78 @@
package app.k9mail.ui.catalog.items
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.grid.LazyGridScope
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import app.k9mail.core.ui.compose.designsystem.atom.Surface
import app.k9mail.core.ui.compose.designsystem.atom.text.TextBody1
import app.k9mail.core.ui.compose.theme.MainTheme
fun LazyGridScope.colorItems() {
sectionHeaderItem(text = "Colors")
item {
ColorContent(
name = "Primary",
color = MainTheme.colors.primary,
)
}
item {
ColorContent(
name = "Primary Variant",
color = MainTheme.colors.primaryVariant,
)
}
item {
ColorContent(
name = "Secondary",
color = MainTheme.colors.secondary,
)
}
item {
ColorContent(
name = "Secondary Variant",
color = MainTheme.colors.secondaryVariant,
)
}
item {
ColorContent(
name = "Background",
color = MainTheme.colors.background,
)
}
item {
ColorContent(
name = "Surface",
color = MainTheme.colors.surface,
)
}
item {
ColorContent(
name = "Error",
color = MainTheme.colors.error,
)
}
}
@Composable
private fun ColorContent(
name: String,
color: Color,
modifier: Modifier = Modifier,
) {
Surface(
color = color,
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center,
modifier = modifier.padding(MainTheme.spacings.double),
) {
TextBody1(text = name)
}
}
}

View file

@ -0,0 +1,13 @@
package app.k9mail.ui.catalog.items
import androidx.compose.foundation.Image
import androidx.compose.foundation.lazy.grid.LazyGridScope
import androidx.compose.ui.res.painterResource
import app.k9mail.core.ui.compose.theme.MainTheme
fun LazyGridScope.imageItems() {
sectionHeaderItem(text = "Images")
item {
Image(painter = painterResource(id = MainTheme.images.logo), contentDescription = "logo")
}
}

View file

@ -0,0 +1,16 @@
package app.k9mail.ui.catalog.items
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.grid.GridItemSpan
import androidx.compose.foundation.lazy.grid.LazyGridScope
import androidx.compose.ui.Modifier
import app.k9mail.core.ui.compose.designsystem.atom.text.TextHeadline6
import app.k9mail.core.ui.compose.theme.MainTheme
fun LazyGridScope.sectionHeaderItem(
text: String,
) {
item(span = { GridItemSpan(maxLineSpan) }) {
TextHeadline6(text = text, modifier = Modifier.padding(top = MainTheme.spacings.default))
}
}

View file

@ -0,0 +1,16 @@
package app.k9mail.ui.catalog.items
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.grid.GridItemSpan
import androidx.compose.foundation.lazy.grid.LazyGridScope
import androidx.compose.ui.Modifier
import app.k9mail.core.ui.compose.designsystem.atom.text.TextSubtitle1
import app.k9mail.core.ui.compose.theme.MainTheme
fun LazyGridScope.sectionSubtitleItem(
text: String,
) {
item(span = { GridItemSpan(maxLineSpan) }) {
TextSubtitle1(text = text, modifier = Modifier.padding(top = MainTheme.spacings.default))
}
}

View file

@ -0,0 +1,39 @@
package app.k9mail.ui.catalog.items
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.lazy.grid.LazyGridScope
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import app.k9mail.core.ui.compose.designsystem.atom.Checkbox
import app.k9mail.core.ui.compose.designsystem.atom.text.TextCaption
fun LazyGridScope.selectionControlItems() {
sectionHeaderItem(text = "Selection Controls")
sectionSubtitleItem(text = "Checkbox")
captionItem(caption = "Checked") {
Checkbox(checked = true, onCheckedChange = {})
}
captionItem(caption = "Unchecked") {
Checkbox(checked = false, onCheckedChange = {})
}
captionItem(caption = "Disabled Checked") {
Checkbox(checked = true, onCheckedChange = {}, enabled = false)
}
captionItem(caption = "Disabled") {
Checkbox(checked = false, onCheckedChange = {}, enabled = false)
}
}
private fun LazyGridScope.captionItem(
caption: String,
content: @Composable () -> Unit,
) {
item {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
) {
content()
TextCaption(text = caption)
}
}
}

View file

@ -0,0 +1,90 @@
package app.k9mail.ui.catalog.items
import androidx.compose.foundation.lazy.grid.LazyGridScope
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import app.k9mail.core.ui.compose.designsystem.atom.textfield.PasswordTextFieldOutlined
import app.k9mail.core.ui.compose.designsystem.atom.textfield.TextFieldOutlined
fun LazyGridScope.textFieldItems() {
sectionHeaderItem(text = "Text fields")
textFieldOutlinedItems()
passwordTextFieldOutlinedItems()
}
private fun LazyGridScope.textFieldOutlinedItems() {
sectionSubtitleItem(text = "Outlined")
item {
WithRememberedInput(text = "Initial text") { input ->
TextFieldOutlined(
value = input.value,
label = "Label",
onValueChange = { input.value = it },
)
}
}
item {
WithRememberedInput(text = "Input text with error") { input ->
TextFieldOutlined(
value = input.value,
label = "Label",
onValueChange = { input.value = it },
isError = true,
)
}
}
item {
WithRememberedInput(text = "Input text disabled") { input ->
TextFieldOutlined(
value = input.value,
label = "Label",
onValueChange = { input.value = it },
enabled = false,
)
}
}
}
private fun LazyGridScope.passwordTextFieldOutlinedItems() {
sectionSubtitleItem(text = "Password outlined")
item {
WithRememberedInput(text = "Input text") { input ->
PasswordTextFieldOutlined(
value = input.value,
label = "Label",
onValueChange = { input.value = it },
)
}
}
item {
WithRememberedInput(text = "Input text with error") { input ->
PasswordTextFieldOutlined(
value = input.value,
label = "Label",
onValueChange = { input.value = it },
isError = true,
)
}
}
item {
WithRememberedInput(text = "Input text disabled") { input ->
PasswordTextFieldOutlined(
value = input.value,
label = "Label",
onValueChange = { input.value = it },
enabled = false,
)
}
}
}
@Composable
private fun WithRememberedInput(
text: String,
content: @Composable (text: MutableState<String>) -> Unit,
) {
val inputText = remember { mutableStateOf(text) }
content(inputText)
}

View file

@ -0,0 +1,16 @@
package app.k9mail.ui.catalog.items
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.grid.GridItemSpan
import androidx.compose.foundation.lazy.grid.LazyGridScope
import androidx.compose.ui.Modifier
import app.k9mail.core.ui.compose.designsystem.atom.text.TextHeadline4
import app.k9mail.core.ui.compose.theme.MainTheme
fun LazyGridScope.themeHeaderItem(
text: String,
) {
item(span = { GridItemSpan(maxLineSpan) }) {
TextHeadline4(text = text, modifier = Modifier.padding(top = MainTheme.spacings.default))
}
}

View file

@ -0,0 +1,31 @@
package app.k9mail.ui.catalog.items
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.lazy.grid.LazyGridScope
import androidx.compose.ui.Modifier
import app.k9mail.ui.catalog.CatalogTheme
import app.k9mail.ui.catalog.CatalogThemeSelector
import app.k9mail.ui.catalog.CatalogThemeVariant
import app.k9mail.ui.catalog.CatalogThemeVariantSelector
fun LazyGridScope.themeSelectorItems(
catalogTheme: CatalogTheme,
catalogThemeVariant: CatalogThemeVariant,
onThemeChange: () -> Unit,
onThemeVariantChange: () -> Unit,
) {
item {
CatalogThemeSelector(
catalogTheme = catalogTheme,
modifier = Modifier.fillMaxWidth(),
onThemeChangeClick = onThemeChange,
)
}
item {
CatalogThemeVariantSelector(
catalogThemeVariant = catalogThemeVariant,
modifier = Modifier.fillMaxWidth(),
onThemeVariantChange = onThemeVariantChange,
)
}
}

View file

@ -0,0 +1,33 @@
package app.k9mail.ui.catalog.items
import androidx.compose.foundation.lazy.grid.LazyGridScope
import app.k9mail.core.ui.compose.designsystem.atom.text.TextBody1
import app.k9mail.core.ui.compose.designsystem.atom.text.TextBody2
import app.k9mail.core.ui.compose.designsystem.atom.text.TextButton
import app.k9mail.core.ui.compose.designsystem.atom.text.TextCaption
import app.k9mail.core.ui.compose.designsystem.atom.text.TextHeadline1
import app.k9mail.core.ui.compose.designsystem.atom.text.TextHeadline2
import app.k9mail.core.ui.compose.designsystem.atom.text.TextHeadline3
import app.k9mail.core.ui.compose.designsystem.atom.text.TextHeadline4
import app.k9mail.core.ui.compose.designsystem.atom.text.TextHeadline5
import app.k9mail.core.ui.compose.designsystem.atom.text.TextHeadline6
import app.k9mail.core.ui.compose.designsystem.atom.text.TextOverline
import app.k9mail.core.ui.compose.designsystem.atom.text.TextSubtitle1
import app.k9mail.core.ui.compose.designsystem.atom.text.TextSubtitle2
fun LazyGridScope.typographyItems() {
sectionHeaderItem(text = "Typography")
item { TextHeadline1(text = "Headline1") }
item { TextHeadline2(text = "Headline2") }
item { TextHeadline3(text = "Headline3") }
item { TextHeadline4(text = "Headline4") }
item { TextHeadline5(text = "Headline5") }
item { TextHeadline6(text = "Headline6") }
item { TextSubtitle1(text = "Subtitle1") }
item { TextSubtitle2(text = "Subtitle2") }
item { TextBody1(text = "Body1") }
item { TextBody2(text = "Body2") }
item { TextButton(text = "Button") }
item { TextCaption(text = "Caption") }
item { TextOverline(text = "Overline") }
}

View file

@ -0,0 +1,149 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp"
android:height="108dp"
android:viewportWidth="192"
android:viewportHeight="192">
<group android:scaleX="0.52411765"
android:scaleY="0.52411765"
android:translateX="45.684708"
android:translateY="44.75294">
<path
android:pathData="M50,12C46.68,12 44,14.68 44,18V26C44,29.32 46.68,32 50,32H64V48H72V32H74C77.32,32 80,29.32 80,26V18C80,14.68 77.32,12 74,12H50ZM118,12C114.68,12 112,14.68 112,18V26C112,29.32 114.68,32 118,32H120V48H128V32H142C145.32,32 148,29.32 148,26V18C148,14.68 145.32,12 142,12H118ZM32,120V132L57.61,170C59.68,173.59 63.54,176 68,176H124C128.46,176 132.32,173.59 134.39,170H134.4L160,132V120H32Z"
android:strokeAlpha="0.2"
android:fillColor="#000000"
android:fillAlpha="0.2"/>
<path
android:pathData="M50,8C46.68,8 44,10.68 44,14V22C44,25.32 46.68,28 50,28H64V44H72V28H74C77.32,28 80,25.32 80,22V14C80,10.68 77.32,8 74,8H50ZM118,8C114.68,8 112,10.68 112,14V22C112,25.32 114.68,28 118,28H120V44H128V28H142C145.32,28 148,25.32 148,22V14C148,10.68 145.32,8 142,8H118ZM32,116V128L57.61,166C59.68,169.59 63.54,172 68,172H124C128.46,172 132.32,169.59 134.39,166H134.4L160,128V116H32Z"
android:strokeAlpha="0.2"
android:fillColor="#000000"
android:fillAlpha="0.2"/>
<path
android:pathData="M24,116L32,128L57.61,166C59.68,169.59 63.54,172 68,172H124C128.46,172 132.32,169.59 134.39,166H134.4L160,128L168,116H24Z"
android:strokeAlpha="0.2"
android:fillColor="#000000"
android:fillAlpha="0.2"/>
<path
android:pathData="M32,116V128L57.61,166C59.68,169.59 63.54,172 68,172H124C128.46,172 132.32,169.59 134.39,166H134.4L160.01,128V116H32Z"
android:fillColor="#607D8B"
android:fillType="evenOdd"/>
<path
android:pathData="M72,16H64V44H72V16Z"
android:fillColor="#263238"/>
<path
android:pathData="M128,16H120V44H128V16Z"
android:fillColor="#263238"/>
<path
android:pathData="M32,127V128L57.61,166C59.68,169.59 63.54,172 68,172H124C128.46,172 132.32,169.59 134.39,166H134.4L160,128V127L134.4,165H134.39C132.32,168.59 128.46,171 124,171H68C63.54,171 59.68,168.59 57.61,165L32,127Z"
android:fillColor="#4D6570"/>
<path
android:pathData="M80,22V14C80,10.69 77.31,8 74,8L50,8C46.69,8 44,10.69 44,14V22C44,25.31 46.69,28 50,28L74,28C77.31,28 80,25.31 80,22Z"
android:fillColor="#607D8B"/>
<path
android:pathData="M148,22V14C148,10.69 145.31,8 142,8L118,8C114.69,8 112,10.69 112,14V22C112,25.31 114.69,28 118,28L142,28C145.31,28 148,25.31 148,22Z"
android:fillColor="#607D8B"/>
<path
android:pathData="M44,21V22C44,25.32 46.68,28 50,28H74C77.32,28 80,25.32 80,22V21C80,24.32 77.32,27 74,27H50C46.68,27 44,24.32 44,21Z"
android:fillColor="#4D6570"/>
<path
android:pathData="M112,21V22C112,25.32 114.68,28 118,28H142C145.32,28 148,25.32 148,22V21C148,24.32 145.32,27 142,27H118C114.68,27 112,24.32 112,21Z"
android:fillColor="#4D6570"/>
<path
android:pathData="M50,8C46.68,8 44,10.68 44,14V15C44,11.68 46.68,9 50,9H74C77.32,9 80,11.68 80,15V14C80,10.68 77.32,8 74,8H50Z"
android:fillColor="#8097A2"/>
<path
android:pathData="M118,8C114.68,8 112,10.68 112,14V15C112,11.68 114.68,9 118,9H142C145.32,9 148,11.68 148,15V14C148,10.68 145.32,8 142,8H118Z"
android:fillColor="#8097A2"/>
<path
android:pathData="M32,116V128L57.61,166C59.68,169.59 63.54,172 68,172H124C128.46,172 132.32,169.59 134.39,166H134.4L160,128V116H32Z"
android:fillType="evenOdd">
<aapt:attr name="android:fillColor">
<gradient
android:startX="80"
android:startY="112"
android:endX="80"
android:endY="140"
android:type="linear">
<item android:offset="0" android:color="#FF4D6570"/>
<item android:offset="1" android:color="#0F4D6570"/>
</gradient>
</aapt:attr>
</path>
<path
android:pathData="M171.99,120V52C171.99,45.37 166.62,40 159.99,40L31.99,40C25.37,40 19.99,45.37 19.99,52V120C19.99,126.62 25.37,132 31.99,132H159.99C166.62,132 171.99,126.62 171.99,120Z"
android:strokeAlpha="0.2"
android:fillColor="#37abc8"
android:fillAlpha="0.2"/>
<path
android:pathData="M171.99,116V48C171.99,41.37 166.62,36 159.99,36L31.99,36C25.37,36 19.99,41.37 19.99,48V116C19.99,122.62 25.37,128 31.99,128L159.99,128C166.62,128 171.99,122.62 171.99,116Z"
android:strokeAlpha="0.2"
android:fillColor="#5fbcd3"
android:fillAlpha="0.2"/>
<path
android:pathData="M172,116V48C172,41.37 166.63,36 160,36L32,36C25.37,36 20,41.37 20,48V116C20,122.63 25.37,128 32,128H160C166.63,128 172,122.63 172,116Z"
android:fillColor="#ff9955"/>
<path
android:pathData="M36,52L96,84L156,52"
android:strokeWidth="6"
android:fillColor="#00000000"
android:strokeColor="#FBE9E7"
android:strokeLineCap="round"/>
<path
android:pathData="M32,36C25.35,36 20,41.35 20,48V49C20,42.35 25.35,37 32,37H160C166.65,37 172,42.35 172,49V48C172,41.35 166.65,36 160,36H32Z"
android:fillColor="#ff9955"/>
<path
android:pathData="M20,115V116C20,122.65 25.35,128 32,128H160C166.65,128 172,122.65 172,116V115C172,121.65 166.65,127 160,127H32C25.35,127 20,121.65 20,115Z"
android:fillColor="#ff7f2a"/>
<path
android:pathData="M90,156C86.68,156 84,158.68 84,162V174C84,174.27 84.03,174.54 84.06,174.8C84.06,174.8 84.06,174.81 84.06,174.81C84.02,175.2 84,175.6 84,176C84,179.18 85.26,182.23 87.51,184.48C89.77,186.73 92.82,188 96,188C99.18,188 102.24,186.73 104.49,184.48C106.74,182.23 108,179.18 108,176C108,175.61 107.97,175.23 107.93,174.85C107.97,174.57 108,174.29 108,174V162C108,158.67 105.33,156 102,156L90,156Z"
android:strokeAlpha="0.2"
android:fillColor="#000000"
android:fillAlpha="0.2"/>
<path
android:pathData="M90,152C86.68,152 84,154.68 84,158V170C84,170.27 84.03,170.54 84.06,170.8C84.06,170.8 84.06,170.81 84.06,170.81C84.02,171.2 84,171.6 84,172C84,175.18 85.26,178.23 87.51,180.48C89.77,182.73 92.82,184 96,184C99.18,184 102.24,182.73 104.49,180.48C106.74,178.23 108,175.18 108,172C108,171.61 107.97,171.23 107.93,170.85C107.97,170.57 108,170.29 108,170V158C108,154.67 105.33,152 102,152L90,152Z"
android:strokeAlpha="0.2"
android:fillColor="#000000"
android:fillAlpha="0.2"/>
<path
android:pathData="M108,170V158C108,154.69 105.31,152 102,152H90C86.69,152 84,154.69 84,158V170C84,173.31 86.69,176 90,176H102C105.31,176 108,173.31 108,170Z"
android:fillColor="#263238"/>
<path
android:pathData="M96,184C102.63,184 108,178.63 108,172C108,165.37 102.63,160 96,160C89.37,160 84,165.37 84,172C84,178.63 89.37,184 96,184Z"
android:fillColor="#263238"/>
<path
android:pathData="M90,152C86.68,152 84,154.68 84,158V159C84,155.68 86.68,153 90,153H102C105.32,153 108,155.68 108,159V158C108,154.68 105.32,152 102,152H90Z"
android:fillColor="#37474F"/>
<path
android:pathData="M84.02,171.43C84.01,171.62 84,171.81 84,172C84,175.18 85.26,178.24 87.51,180.49C89.77,182.74 92.82,184 96,184C99.18,184 102.24,182.74 104.49,180.49C106.74,178.24 108,175.18 108,172C108,171.86 107.99,171.73 107.98,171.59C107.83,174.67 106.5,177.57 104.27,179.69C102.04,181.81 99.08,183 96,183C92.89,183 89.91,181.79 87.68,179.63C85.44,177.47 84.13,174.53 84.02,171.43Z"
android:fillColor="#1A252A"/>
<path
android:pathData="M50,8C46.68,8 44,10.68 44,14V22C44,25.32 46.68,28 50,28H64V36H32C25.35,36 20,41.35 20,48V116C20,122.65 25.35,128 32,128L57.61,166C59.68,169.59 63.54,172 68,172H84C84,175.18 85.26,178.24 87.51,180.49C89.77,182.74 92.82,184 96,184C99.18,184 102.23,182.74 104.48,180.49C106.73,178.23 108,175.18 108,172H124C128.46,172 132.32,169.59 134.39,166H134.4L160,128C166.65,128 172,122.65 172,116V48C172,41.35 166.65,36 160,36H128V28H142C145.32,28 148,25.32 148,22V14C148,10.68 145.32,8 142,8H118C114.68,8 112,10.68 112,14V22C112,25.32 114.68,28 118,28H120V36H72V28H74C77.32,28 80,25.32 80,22V14C80,10.68 77.32,8 74,8H50Z"
android:fillType="evenOdd">
<aapt:attr name="android:fillColor">
<gradient
android:centerX="-0"
android:centerY="0"
android:gradientRadius="271.53"
android:type="radial">
<item android:offset="0" android:color="#19FBFCFC"/>
<item android:offset="1" android:color="#00FBFCFC"/>
</gradient>
</aapt:attr>
</path>
<path
android:pathData="m50,8.72c-3.32,0 -6,2.68 -6,6v8c0,3.32 2.68,6 6,6h14v8H32c-3.34,0 -6.35,1.35 -8.53,3.54C21.32,42.43 20,45.41 20,48.72v68c0,6.65 5.35,12 12,12l25.61,38c2.07,3.59 5.94,6 10.39,6h16c0,3.18 1.26,6.24 3.51,8.48 2.25,2.25 5.3,3.52 8.49,3.52 3.18,-0 6.23,-1.26 8.48,-3.52 2.25,-2.25 3.51,-5.3 3.51,-8.48H124c4.46,0 8.32,-2.41 10.39,-6h0.01l25.6,-38c6.65,-0 12,-5.35 12,-12v-68c0,-6.65 -5.35,-12 -12,-12h-32v-8h14c3.32,0 6,-2.68 6,-6v-8c0,-3.32 -2.68,-6 -6,-6h-24c-3.32,0 -6,2.68 -6,6v8c0,3.32 2.68,6 6,6h2v8H72v-8h2c3.32,0 6,-2.68 6,-6v-8c0,-3.32 -2.68,-6 -6,-6z"
android:fillType="evenOdd">
<aapt:attr name="android:fillColor">
<gradient
android:startX="192"
android:startY="0.72"
android:endX="192"
android:endY="192.72"
android:type="linear">
<item android:offset="0" android:color="#19FBFCFC"/>
<item android:offset="1" android:color="#00FBFCFC"/>
</gradient>
</aapt:attr>
</path>
</group>
</vector>

View file

@ -0,0 +1,104 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="192"
android:viewportHeight="192">
<group
android:scaleX="0.52411765"
android:scaleY="0.52411765"
android:translateX="45.684708"
android:translateY="44.75294">
<path
android:fillAlpha="0.2"
android:fillColor="#000000"
android:pathData="M50,12C46.68,12 44,14.68 44,18V26C44,29.32 46.68,32 50,32H64V48H72V32H74C77.32,32 80,29.32 80,26V18C80,14.68 77.32,12 74,12H50ZM118,12C114.68,12 112,14.68 112,18V26C112,29.32 114.68,32 118,32H120V48H128V32H142C145.32,32 148,29.32 148,26V18C148,14.68 145.32,12 142,12H118ZM32,120V132L57.61,170C59.68,173.59 63.54,176 68,176H124C128.46,176 132.32,173.59 134.39,170H134.4L160,132V120H32Z"
android:strokeAlpha="0.2" />
<path
android:fillAlpha="0.2"
android:fillColor="#000000"
android:pathData="M50,8C46.68,8 44,10.68 44,14V22C44,25.32 46.68,28 50,28H64V44H72V28H74C77.32,28 80,25.32 80,22V14C80,10.68 77.32,8 74,8H50ZM118,8C114.68,8 112,10.68 112,14V22C112,25.32 114.68,28 118,28H120V44H128V28H142C145.32,28 148,25.32 148,22V14C148,10.68 145.32,8 142,8H118ZM32,116V128L57.61,166C59.68,169.59 63.54,172 68,172H124C128.46,172 132.32,169.59 134.39,166H134.4L160,128V116H32Z"
android:strokeAlpha="0.2" />
<path
android:fillAlpha="0.2"
android:fillColor="#000000"
android:pathData="M24,116L32,128L57.61,166C59.68,169.59 63.54,172 68,172H124C128.46,172 132.32,169.59 134.39,166H134.4L160,128L168,116H24Z"
android:strokeAlpha="0.2" />
<path
android:fillColor="#607D8B"
android:pathData="M32,116V128L57.61,166C59.68,169.59 63.54,172 68,172H124C128.46,172 132.32,169.59 134.39,166H134.4L160.01,128V116H32Z" />
<path
android:fillColor="#263238"
android:pathData="M72,16H64V44H72V16Z" />
<path
android:fillColor="#263238"
android:pathData="M128,16H120V44H128V16Z" />
<path
android:fillColor="#4D6570"
android:pathData="M32,127V128L57.61,166C59.68,169.59 63.54,172 68,172H124C128.46,172 132.32,169.59 134.39,166H134.4L160,128V127L134.4,165H134.39C132.32,168.59 128.46,171 124,171H68C63.54,171 59.68,168.59 57.61,165L32,127Z" />
<path
android:fillColor="#607D8B"
android:pathData="M80,22V14C80,10.69 77.31,8 74,8L50,8C46.69,8 44,10.69 44,14V22C44,25.31 46.69,28 50,28L74,28C77.31,28 80,25.31 80,22Z" />
<path
android:fillColor="#607D8B"
android:pathData="M148,22V14C148,10.69 145.31,8 142,8L118,8C114.69,8 112,10.69 112,14V22C112,25.31 114.69,28 118,28L142,28C145.31,28 148,25.31 148,22Z" />
<path
android:fillColor="#4D6570"
android:pathData="M44,21V22C44,25.32 46.68,28 50,28H74C77.32,28 80,25.32 80,22V21C80,24.32 77.32,27 74,27H50C46.68,27 44,24.32 44,21Z" />
<path
android:fillColor="#4D6570"
android:pathData="M112,21V22C112,25.32 114.68,28 118,28H142C145.32,28 148,25.32 148,22V21C148,24.32 145.32,27 142,27H118C114.68,27 112,24.32 112,21Z" />
<path
android:fillColor="#8097A2"
android:pathData="M50,8C46.68,8 44,10.68 44,14V15C44,11.68 46.68,9 50,9H74C77.32,9 80,11.68 80,15V14C80,10.68 77.32,8 74,8H50Z" />
<path
android:fillColor="#8097A2"
android:pathData="M118,8C114.68,8 112,10.68 112,14V15C112,11.68 114.68,9 118,9H142C145.32,9 148,11.68 148,15V14C148,10.68 145.32,8 142,8H118Z" />
<path
android:fillAlpha="0.2"
android:fillColor="#37abc8"
android:pathData="M171.99,120V52C171.99,45.37 166.62,40 159.99,40L31.99,40C25.37,40 19.99,45.37 19.99,52V120C19.99,126.62 25.37,132 31.99,132H159.99C166.62,132 171.99,126.62 171.99,120Z"
android:strokeAlpha="0.2" />
<path
android:fillAlpha="0.2"
android:fillColor="#5fbcd3"
android:pathData="M171.99,116V48C171.99,41.37 166.62,36 159.99,36L31.99,36C25.37,36 19.99,41.37 19.99,48V116C19.99,122.62 25.37,128 31.99,128L159.99,128C166.62,128 171.99,122.62 171.99,116Z"
android:strokeAlpha="0.2" />
<path
android:fillColor="#ff9955"
android:pathData="M172,116V48C172,41.37 166.63,36 160,36L32,36C25.37,36 20,41.37 20,48V116C20,122.63 25.37,128 32,128H160C166.63,128 172,122.63 172,116Z" />
<path
android:fillColor="#00000000"
android:pathData="M36,52L96,84L156,52"
android:strokeWidth="6"
android:strokeColor="#FBE9E7"
android:strokeLineCap="round" />
<path
android:fillColor="#ff9955"
android:pathData="M32,36C25.35,36 20,41.35 20,48V49C20,42.35 25.35,37 32,37H160C166.65,37 172,42.35 172,49V48C172,41.35 166.65,36 160,36H32Z" />
<path
android:fillColor="#ff7f2a"
android:pathData="M20,115V116C20,122.65 25.35,128 32,128H160C166.65,128 172,122.65 172,116V115C172,121.65 166.65,127 160,127H32C25.35,127 20,121.65 20,115Z" />
<path
android:fillAlpha="0.2"
android:fillColor="#000000"
android:pathData="M90,156C86.68,156 84,158.68 84,162V174C84,174.27 84.03,174.54 84.06,174.8C84.06,174.8 84.06,174.81 84.06,174.81C84.02,175.2 84,175.6 84,176C84,179.18 85.26,182.23 87.51,184.48C89.77,186.73 92.82,188 96,188C99.18,188 102.24,186.73 104.49,184.48C106.74,182.23 108,179.18 108,176C108,175.61 107.97,175.23 107.93,174.85C107.97,174.57 108,174.29 108,174V162C108,158.67 105.33,156 102,156L90,156Z"
android:strokeAlpha="0.2" />
<path
android:fillAlpha="0.2"
android:fillColor="#000000"
android:pathData="M90,152C86.68,152 84,154.68 84,158V170C84,170.27 84.03,170.54 84.06,170.8C84.06,170.8 84.06,170.81 84.06,170.81C84.02,171.2 84,171.6 84,172C84,175.18 85.26,178.23 87.51,180.48C89.77,182.73 92.82,184 96,184C99.18,184 102.24,182.73 104.49,180.48C106.74,178.23 108,175.18 108,172C108,171.61 107.97,171.23 107.93,170.85C107.97,170.57 108,170.29 108,170V158C108,154.67 105.33,152 102,152L90,152Z"
android:strokeAlpha="0.2" />
<path
android:fillColor="#263238"
android:pathData="M108,170V158C108,154.69 105.31,152 102,152H90C86.69,152 84,154.69 84,158V170C84,173.31 86.69,176 90,176H102C105.31,176 108,173.31 108,170Z" />
<path
android:fillColor="#263238"
android:pathData="M96,184C102.63,184 108,178.63 108,172C108,165.37 102.63,160 96,160C89.37,160 84,165.37 84,172C84,178.63 89.37,184 96,184Z" />
<path
android:fillColor="#37474F"
android:pathData="M90,152C86.68,152 84,154.68 84,158V159C84,155.68 86.68,153 90,153H102C105.32,153 108,155.68 108,159V158C108,154.68 105.32,152 102,152H90Z" />
<path
android:fillColor="#1A252A"
android:pathData="M84.02,171.43C84.01,171.62 84,171.81 84,172C84,175.18 85.26,178.24 87.51,180.49C89.77,182.74 92.82,184 96,184C99.18,184 102.24,182.74 104.49,180.49C106.74,178.24 108,175.18 108,172C108,171.86 107.99,171.73 107.98,171.59C107.83,174.67 106.5,177.57 104.27,179.69C102.04,181.81 99.08,183 96,183C92.89,183 89.91,181.79 87.68,179.63C85.44,177.47 84.13,174.53 84.02,171.43Z" />
</group>
</vector>

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
</adaptive-icon>

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
</adaptive-icon>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View file

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_launcher_background">#FCE8DC</color>
</resources>

View file

@ -0,0 +1,3 @@
<resources>
<string name="app_name">Thunderbird Catalog</string>
</resources>

View file

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="Theme.Thunderbird" parent="android:Theme.Material.Light.NoActionBar" />
</resources>

View file

@ -0,0 +1,9 @@
@Suppress("DSL_SCOPE_VIOLATION")
plugins {
id(ThunderbirdPlugins.Library.jvm)
alias(libs.plugins.android.lint)
}
dependencies {
api(projects.mail.common)
}

View file

@ -0,0 +1,19 @@
package com.fsck.k9.autodiscovery.api
import com.fsck.k9.mail.AuthType
import com.fsck.k9.mail.ConnectionSecurity
interface ConnectionSettingsDiscovery {
fun discover(email: String): DiscoveryResults?
}
data class DiscoveryResults(val incoming: List<DiscoveredServerSettings>, val outgoing: List<DiscoveredServerSettings>)
data class DiscoveredServerSettings(
val protocol: String,
val host: String,
val port: Int,
val security: ConnectionSecurity,
val authType: AuthType?,
val username: String?
)

View file

@ -0,0 +1,20 @@
plugins {
id(ThunderbirdPlugins.Library.android)
}
dependencies {
implementation(projects.app.core)
implementation(projects.mail.common)
implementation(projects.app.autodiscovery.api)
implementation(libs.timber)
testImplementation(projects.app.testing)
testImplementation(projects.backend.imap)
testImplementation(libs.robolectric)
testImplementation(libs.androidx.test.core)
}
android {
namespace = "com.fsck.k9.autodiscovery.providersxml"
}

View file

@ -0,0 +1,8 @@
package com.fsck.k9.autodiscovery.providersxml
import org.koin.dsl.module
val autodiscoveryProvidersXmlModule = module {
factory { ProvidersXmlProvider(context = get()) }
factory { ProvidersXmlDiscovery(xmlProvider = get(), oAuthConfigurationProvider = get()) }
}

View file

@ -0,0 +1,156 @@
package com.fsck.k9.autodiscovery.providersxml
import android.content.res.XmlResourceParser
import android.net.Uri
import com.fsck.k9.autodiscovery.api.ConnectionSettingsDiscovery
import com.fsck.k9.autodiscovery.api.DiscoveredServerSettings
import com.fsck.k9.autodiscovery.api.DiscoveryResults
import com.fsck.k9.helper.EmailHelper
import com.fsck.k9.mail.AuthType
import com.fsck.k9.mail.ConnectionSecurity
import com.fsck.k9.oauth.OAuthConfigurationProvider
import com.fsck.k9.preferences.Protocols
import org.xmlpull.v1.XmlPullParser
import timber.log.Timber
class ProvidersXmlDiscovery(
private val xmlProvider: ProvidersXmlProvider,
private val oAuthConfigurationProvider: OAuthConfigurationProvider
) : ConnectionSettingsDiscovery {
override fun discover(email: String): DiscoveryResults? {
val domain = EmailHelper.getDomainFromEmailAddress(email) ?: return null
val provider = findProviderForDomain(domain) ?: return null
val incomingSettings = provider.toIncomingServerSettings(email) ?: return null
val outgoingSettings = provider.toOutgoingServerSettings(email) ?: return null
return DiscoveryResults(listOf(incomingSettings), listOf(outgoingSettings))
}
private fun findProviderForDomain(domain: String): Provider? {
return try {
xmlProvider.getXml().use { xml ->
parseProviders(xml, domain)
}
} catch (e: Exception) {
Timber.e(e, "Error while trying to load provider settings.")
null
}
}
private fun parseProviders(xml: XmlResourceParser, domain: String): Provider? {
do {
val xmlEventType = xml.next()
if (xmlEventType == XmlPullParser.START_TAG && xml.name == "provider") {
val providerDomain = xml.getAttributeValue(null, "domain")
if (domain.equals(providerDomain, ignoreCase = true)) {
val provider = parseProvider(xml)
if (provider != null) return provider
}
}
} while (xmlEventType != XmlPullParser.END_DOCUMENT)
return null
}
private fun parseProvider(xml: XmlResourceParser): Provider? {
var incomingUriTemplate: String? = null
var incomingUsernameTemplate: String? = null
var outgoingUriTemplate: String? = null
var outgoingUsernameTemplate: String? = null
do {
val xmlEventType = xml.next()
if (xmlEventType == XmlPullParser.START_TAG) {
when (xml.name) {
"incoming" -> {
incomingUriTemplate = xml.getAttributeValue(null, "uri")
incomingUsernameTemplate = xml.getAttributeValue(null, "username")
}
"outgoing" -> {
outgoingUriTemplate = xml.getAttributeValue(null, "uri")
outgoingUsernameTemplate = xml.getAttributeValue(null, "username")
}
}
}
} while (!(xmlEventType == XmlPullParser.END_TAG && xml.name == "provider"))
return if (incomingUriTemplate != null && incomingUsernameTemplate != null && outgoingUriTemplate != null &&
outgoingUsernameTemplate != null
) {
Provider(incomingUriTemplate, incomingUsernameTemplate, outgoingUriTemplate, outgoingUsernameTemplate)
} else {
null
}
}
private fun Provider.toIncomingServerSettings(email: String): DiscoveredServerSettings? {
val user = EmailHelper.getLocalPartFromEmailAddress(email) ?: return null
val domain = EmailHelper.getDomainFromEmailAddress(email) ?: return null
val username = incomingUsernameTemplate.fillInUsernameTemplate(email, user, domain)
val security = when {
incomingUriTemplate.startsWith("imap+ssl") -> ConnectionSecurity.SSL_TLS_REQUIRED
incomingUriTemplate.startsWith("imap+tls") -> ConnectionSecurity.STARTTLS_REQUIRED
else -> error("Connection security required")
}
val uri = Uri.parse(incomingUriTemplate)
val host = uri.host ?: error("Host name required")
val port = if (uri.port == -1) {
if (security == ConnectionSecurity.STARTTLS_REQUIRED) 143 else 993
} else {
uri.port
}
val authType = if (oAuthConfigurationProvider.getConfiguration(host) != null) {
AuthType.XOAUTH2
} else {
AuthType.PLAIN
}
return DiscoveredServerSettings(Protocols.IMAP, host, port, security, authType, username)
}
private fun Provider.toOutgoingServerSettings(email: String): DiscoveredServerSettings? {
val user = EmailHelper.getLocalPartFromEmailAddress(email) ?: return null
val domain = EmailHelper.getDomainFromEmailAddress(email) ?: return null
val username = outgoingUsernameTemplate.fillInUsernameTemplate(email, user, domain)
val security = when {
outgoingUriTemplate.startsWith("smtp+ssl") -> ConnectionSecurity.SSL_TLS_REQUIRED
outgoingUriTemplate.startsWith("smtp+tls") -> ConnectionSecurity.STARTTLS_REQUIRED
else -> error("Connection security required")
}
val uri = Uri.parse(outgoingUriTemplate)
val host = uri.host ?: error("Host name required")
val port = if (uri.port == -1) {
if (security == ConnectionSecurity.STARTTLS_REQUIRED) 587 else 465
} else {
uri.port
}
val authType = if (oAuthConfigurationProvider.getConfiguration(host) != null) {
AuthType.XOAUTH2
} else {
AuthType.PLAIN
}
return DiscoveredServerSettings(Protocols.SMTP, host, port, security, authType, username)
}
private fun String.fillInUsernameTemplate(email: String, user: String, domain: String): String {
return this.replace("\$email", email).replace("\$user", user).replace("\$domain", domain)
}
internal data class Provider(
val incomingUriTemplate: String,
val incomingUsernameTemplate: String,
val outgoingUriTemplate: String,
val outgoingUsernameTemplate: String
)
}

View file

@ -0,0 +1,10 @@
package com.fsck.k9.autodiscovery.providersxml
import android.content.Context
import android.content.res.XmlResourceParser
class ProvidersXmlProvider(private val context: Context) {
fun getXml(): XmlResourceParser {
return context.resources.getXml(R.xml.providers)
}
}

View file

@ -0,0 +1,774 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2008 The Android Open Source Project
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.
-->
<!--
This file is used to specify providers that we know default settings for
so that the user can set up their account by simply entering their email
address and password.
When a user starts this process, the email address is parsed, the domain
broken out and used to search this file for a provider. If one is found the
provider's settings are used to attempt to connect to the account.
At this time, the id and label attributes are not used. However, please include them
if you make edits to this file. id must also be completely unique. label will be shown
to the user when there are multiple options provided for a single domain (not currently
supported).
A provider contains the settings for setting up an email account
that ends with the given domain. Domains should be unique within
this file. Each provider should have at least one incoming section and
one outgoing section. If more than one is specified only the first
will be used.
Valid incoming uri schemes are:
imap+tls+ IMAP with required TLS transport security.
If TLS is not available the connection fails.
imap+ssl+ IMAP with required SSL transport security.
If SSL is not available the connection fails.
Valid outgoing uri schemes are:
smtp+tls+ SMTP with required TLS transport security.
If TLS is not available the connection fails.
smtp+ssl+ SMTP with required SSL transport security.
If SSL is not available the connection fails.
The URIs should be full templates for connection, including a port if
the service uses a non-default port. The default ports are as follows:
imap+tls+ 143 smtp+tls+ 587
imap+ssl+ 993 smtp+ssl+ 465
The username attribute is used to supply a template for the username
that will be presented to the server. This username is built from a
set of variables that are substituted with parts of the user
specified email address.
Valid substitution values for the username attribute are:
$email - the email address the user entered
$user - the value before the @ sign in the email address the user entered
$domain - the value after the @ sign in the email address the user entered
The username attribute MUST be specified for the incoming element, so the IMAP
server can identify the mailbox to be opened.
The username attribute MAY be the empty string for the outgoing element, but only if the
SMTP server supports anonymous transmission (most don't).
While it would technically work please DO NOT add providers that don't support encrypted
connections.
-->
<providers>
<!-- Gmail variants -->
<provider id="gmail" label="Gmail" domain="gmail.com">
<incoming uri="imap+ssl+://imap.gmail.com" username="$email" />
<outgoing uri="smtp+ssl+://smtp.gmail.com" username="$email" />
</provider>
<provider id="googlemail" label="Google Mail" domain="googlemail.com">
<incoming uri="imap+ssl+://imap.googlemail.com" username="$email" />
<outgoing uri="smtp+ssl+://smtp.googlemail.com" username="$email" />
</provider>
<provider id="google" label="Google" domain="google.com">
<incoming uri="imap+ssl+://imap.gmail.com" username="$email" />
<outgoing uri="smtp+ssl+://smtp.gmail.com" username="$email" />
</provider>
<provider id="android" label="Android" domain="android.com">
<incoming uri="imap+ssl+://imap.gmail.com" username="$email" />
<outgoing uri="smtp+ssl+://smtp.gmail.com" username="$email" />
</provider>
<!-- USA -->
<provider id="comcast" label="Comcast" domain="comcast.net">
<incoming uri="imap+ssl+://imap.comcast.net" username="$email" />
<outgoing uri="smtp+tls+://smtp.comcast.net" username="$email" />
</provider>
<provider id="montclair.edu" label="MSU" domain="montclair.edu">
<incoming uri="imap+ssl+://mail.montclair.edu" username="$user" />
<outgoing uri="smtp+tls+://smtp.montclair.edu" username="$user" />
</provider>
<provider id="gmx.com" label="GMX" domain="gmx.com">
<incoming uri="imap+ssl+://imap.gmx.com" username="$email" />
<outgoing uri="smtp+ssl+://mail.gmx.com" username="$email" />
</provider>
<provider id="zoho.com" label="Zoho Mail" domain="zoho.com">
<incoming uri="imap+ssl+://imap.zoho.com" username="$email" />
<outgoing uri="smtp+tls+://smtp.zoho.com" username="$email" />
</provider>
<provider id="riseup" label="Riseup Networks" domain="riseup.net">
<incoming uri="imap+ssl+://mail.riseup.net" username="$user" />
<outgoing uri="smtp+tls+://mail.riseup.net" username="$user" />
</provider>
<!-- Mail.com Variants -->
<provider id="mail.com" label="Mail.com" domain="mail.com">
<incoming uri="imap+ssl+://imap.mail.com" username="$email" />
<outgoing uri="smtp+ssl+://smtp.mail.com" username="$email" />
</provider>
<provider id="email.com" label="Mail.com" domain="email.com">
<incoming uri="imap+ssl+://imap.mail.com" username="$email" />
<outgoing uri="smtp+ssl+://smtp.mail.com" username="$email" />
</provider>
<provider id="techie.com" label="Mail.com" domain="techie.com">
<incoming uri="imap+ssl+://imap.mail.com" username="$email" />
<outgoing uri="smtp+ssl+://smtp.mail.com" username="$email" />
</provider>
<provider id="email.com" label="Mail.com" domain="email.com">
<incoming uri="imap+ssl+://imap.mail.com" username="$email" />
<outgoing uri="smtp+ssl+://smtp.mail.com" username="$email" />
</provider>
<provider id="usa.com" label="Mail.com" domain="usa.com">
<incoming uri="imap+ssl+://imap.mail.com" username="$email" />
<outgoing uri="smtp+ssl+://smtp.mail.com" username="$email" />
</provider>
<provider id="myself.com" label="Mail.com" domain="myself.com">
<incoming uri="imap+ssl+://imap.mail.com" username="$email" />
<outgoing uri="smtp+ssl+://smtp.mail.com" username="$email" />
</provider>
<provider id="consultant.com" label="Mail.com" domain="consultant.com">
<incoming uri="imap+ssl+://imap.mail.com" username="$email" />
<outgoing uri="smtp+ssl+://smtp.mail.com" username="$email" />
</provider>
<provider id="post.com" label="Mail.com" domain="post.com">
<incoming uri="imap+ssl+://imap.mail.com" username="$email" />
<outgoing uri="smtp+ssl+://smtp.mail.com" username="$email" />
</provider>
<provider id="europe.com" label="Mail.com" domain="europe.com">
<incoming uri="imap+ssl+://imap.mail.com" username="$email" />
<outgoing uri="smtp+ssl+://smtp.mail.com" username="$email" />
</provider>
<provider id="asia.com" label="Mail.com" domain="asia.com">
<incoming uri="imap+ssl+://imap.mail.com" username="$email" />
<outgoing uri="smtp+ssl+://smtp.mail.com" username="$email" />
</provider>
<provider id="iname.com" label="Mail.com" domain="iname.com">
<incoming uri="imap+ssl+://imap.mail.com" username="$email" />
<outgoing uri="smtp+ssl+://smtp.mail.com" username="$email" />
</provider>
<provider id="writeme.com" label="Mail.com" domain="writeme.com">
<incoming uri="imap+ssl+://imap.mail.com" username="$email" />
<outgoing uri="smtp+ssl+://smtp.mail.com" username="$email" />
</provider>
<provider id="dr.com" label="Mail.com" domain="dr.com">
<incoming uri="imap+ssl+://imap.mail.com" username="$email" />
<outgoing uri="smtp+ssl+://smtp.mail.com" username="$email" />
</provider>
<provider id="engineer.com" label="Mail.com" domain="engineer.com">
<incoming uri="imap+ssl+://imap.mail.com" username="$email" />
<outgoing uri="smtp+ssl+://smtp.mail.com" username="$email" />
</provider>
<provider id="cheerful.com" label="Mail.com" domain="cheerful.com">
<incoming uri="imap+ssl+://imap.mail.com" username="$email" />
<outgoing uri="smtp+ssl+://smtp.mail.com" username="$email" />
</provider>
<provider id="accountant.com" label="Mail.com" domain="accountant.com">
<incoming uri="imap+ssl+://imap.mail.com" username="$email" />
<outgoing uri="smtp+ssl+://smtp.mail.com" username="$email" />
</provider>
<provider id="techie.com" label="Mail.com" domain="techie.com">
<incoming uri="imap+ssl+://imap.mail.com" username="$email" />
<outgoing uri="smtp+ssl+://smtp.mail.com" username="$email" />
</provider>
<provider id="linuxmail.org" label="Mail.com" domain="linuxmail.com">
<incoming uri="imap+ssl+://imap.mail.com" username="$email" />
<outgoing uri="smtp+ssl+://smtp.mail.com" username="$email" />
</provider>
<provider id="uymail.com" label="Mail.com" domain="uymail.com">
<incoming uri="imap+ssl+://imap.mail.com" username="$email" />
<outgoing uri="smtp+ssl+://smtp.mail.com" username="$email" />
</provider>
<provider id="contractor.net" label="Mail.com" domain="contractor.com">
<incoming uri="imap+ssl+://imap.mail.com" username="$email" />
<outgoing uri="smtp+ssl+://smtp.mail.com" username="$email" />
</provider>
<!-- Yahoo! Mail Variants -->
<provider id="yahoo" label="Yahoo" domain="yahoo.com">
<incoming uri="imap+ssl+://imap.mail.yahoo.com" username="$email" />
<outgoing uri="smtp+ssl+://smtp.mail.yahoo.com" username="$email" />
</provider>
<provider id="yahoo.de" label="Yahoo" domain="yahoo.de">
<incoming uri="imap+ssl+://imap.mail.yahoo.com" username="$email" />
<outgoing uri="smtp+ssl+://smtp.mail.yahoo.com" username="$email" />
</provider>
<provider id="ymail" label="YMail" domain="ymail.com">
<incoming uri="imap+ssl+://imap.mail.yahoo.com" username="$email" />
<outgoing uri="smtp+ssl+://smtp.mail.yahoo.com" username="$email" />
</provider>
<provider id="rocketmail" label="Rocketmail" domain="rocketmail.com">
<incoming uri="imap+ssl+://imap.mail.yahoo.com" username="$email" />
<outgoing uri="smtp+ssl+://smtp.mail.yahoo.com" username="$email" />
</provider>
<!-- Apple -->
<provider id="apple" label="Apple" domain="apple.com">
<incoming uri="imap+ssl+://imap.mail.apple.com" username="$user" />
<outgoing uri="smtp+tls+://smtp.mail.apple.com" username="$user" />
</provider>
<provider id="dotmac" label=".Mac" domain="mac.com">
<incoming uri="imap+ssl+://imap.mail.mac.com" username="$user" />
<outgoing uri="smtp+tls+://smtp.mail.mac.com" username="$user" />
</provider>
<provider id="mobileme" label="MobileMe" domain="me.com">
<incoming uri="imap+ssl+://imap.mail.me.com" username="$user" />
<outgoing uri="smtp+tls+://smtp.mail.me.com" username="$user" />
</provider>
<provider id="icloud" label="iCloud" domain="icloud.com">
<incoming uri="imap+ssl+://imap.mail.icloud.com" username="$user" />
<outgoing uri="smtp+tls+://smtp.mail.icloud.com" username="$user" />
</provider>
<!-- Australia -->
<provider id="fastmail-fm" label="Fastmail" domain="fastmail.fm">
<incoming uri="imap+ssl+://mail.messagingengine.com" username="$email" />
<outgoing uri="smtp+ssl+://mail.messagingengine.com" username="$email" />
</provider>
<!-- Virgin Media variants -->
<provider id="virginmedia.com" label="Virgin Media" domain="virginmedia.com">
<incoming uri="imap+ssl+://imap.virginmedia.com" username="$email" />
<outgoing uri="smtp+ssl+://smtp.virginmedia.com" username="$email" />
</provider>
<provider id="virgin.net" label="Virgin Media" domain="virgin.net">
<incoming uri="imap+ssl+://imap.virginmedia.com" username="$email" />
<outgoing uri="smtp+ssl+://smtp.virginmedia.com" username="$email" />
</provider>
<provider id="blueyonder.co.uk" label="Virgin Media" domain="blueyonder.co.uk">
<incoming uri="imap+ssl+://imap.virginmedia.com" username="$email" />
<outgoing uri="smtp+ssl+://smtp.virginmedia.com" username="$email" />
</provider>
<provider id="ntlworld.com" label="Virgin Media" domain="ntlworld.com">
<incoming uri="imap+ssl+://imap.virginmedia.com" username="$email" />
<outgoing uri="smtp+ssl+://smtp.virginmedia.com" username="$email" />
</provider>
<!-- France -->
<provider id="mailo.com" label="mailo.com" domain="mailo.com">
<incoming uri="imap+ssl+://mail.mailo.com" username="$email" />
<outgoing uri="smtp+ssl+://mail.mailo.com" username="$email" />
</provider>
<provider id="net-c.fr" label="net-c.fr" domain="net-c.fr">
<incoming uri="imap+ssl+://mail.mailo.com" username="$email" />
<outgoing uri="smtp+ssl+://mail.mailo.com" username="$email" />
</provider>
<!-- Germany -->
<provider id="mailbox.org" label="mailbox.org" domain="mailbox.org">
<incoming uri="imap+tls+://imap.mailbox.org" username="$email" />
<outgoing uri="smtp+tls+://smtp.mailbox.org" username="$email" />
</provider>
<provider id="freenet" label="Freenet" domain="freenet.de">
<incoming uri="imap+tls+://mx.freenet.de" username="$email" />
<outgoing uri="smtp+tls+://mx.freenet.de" username="$email" />
</provider>
<provider id="T-Online" label="T-Online" domain="t-online.de">
<incoming uri="imap+ssl+://secureimap.t-online.de" username="$email" />
<outgoing uri="smtp+tls+://securesmtp.t-online.de" username="$email" />
</provider>
<provider id="web.de" label="Web.de" domain="web.de">
<incoming uri="imap+ssl+://imap.web.de" username="$user" />
<outgoing uri="smtp+tls+://smtp.web.de" username="$user" />
</provider>
<provider id="posteo" label="Posteo" domain="posteo.net">
<incoming uri="imap+tls+://posteo.de" username="$email" />
<outgoing uri="smtp+tls+://posteo.de" username="$email" />
</provider>
<provider id="posteo" label="Posteo" domain="posteo.de">
<incoming uri="imap+tls+://posteo.de" username="$email" />
<outgoing uri="smtp+tls+://posteo.de" username="$email" />
</provider>
<provider id="systemliorg" label="systemli.org" domain="systemli.org">
<incoming uri="imap+ssl+://mail.systemli.org" username="$email" />
<outgoing uri="smtp+tls+://mail.systemli.org" username="$email" />
</provider>
<!-- GMX variants -->
<provider id="gmx.net" label="GMX.net" domain="gmx.net">
<incoming uri="imap+ssl+://imap.gmx.net" username="$email" />
<outgoing uri="smtp+tls+://mail.gmx.net" username="$email" />
</provider>
<provider id="gmx.de" label="GMX.de" domain="gmx.de">
<incoming uri="imap+ssl+://imap.gmx.net" username="$email" />
<outgoing uri="smtp+tls+://mail.gmx.net" username="$email" />
</provider>
<provider id="gmx.at" label="GMX.at" domain="gmx.at">
<incoming uri="imap+ssl+://imap.gmx.net" username="$email" />
<outgoing uri="smtp+tls+://mail.gmx.net" username="$email" />
</provider>
<provider id="gmx.ch" label="GMX.ch" domain="gmx.ch">
<incoming uri="imap+ssl+://imap.gmx.net" username="$email" />
<outgoing uri="smtp+tls+://mail.gmx.net" username="$email" />
</provider>
<provider id="gmx.eu" label="GMX.eu" domain="gmx.eu">
<incoming uri="imap+ssl+://imap.gmx.net" username="$email" />
<outgoing uri="smtp+tls+://mail.gmx.net" username="$email" />
</provider>
<!-- Greece -->
<provider id="otenet.gr" label="otenet.gr" domain="otenet.gr">
<incoming uri="imap+ssl+://imap.otenet.gr" username="$email" />
<outgoing uri="smtp+tls+://mailgate.otenet.gr" username="$email" />
</provider>
<provider id="cosmotemail.gr" label="cosmotemail" domain="cosmotemail.gr">
<incoming uri="imap+ssl+://imap.cosmotemail.gr" username="$email" />
<outgoing uri="smtp+tls+://mailgate.cosmotemail.gr" username="$email" />
</provider>
<provider id="mycosmos.gr" label="mycosmos" domain="mycosmos.gr">
<incoming uri="imap+ssl+://mail.mycosmos.gr" username="$email" />
<outgoing uri="smtp+tls+://mail.mycosmos.gr" username="$email" />
</provider>
<provider id="espiv" label="Espiv.net" domain="espiv.net">
<incoming uri="imap+ssl+://mail.espiv.net" username="$email" />
<outgoing uri="smtp+tls+://mail.espiv.net" username="$email" />
</provider>
<provider id="squat" label="Squat.gr" domain="squat.gr">
<incoming uri="imap+ssl+://mail.espiv.net" username="$email" />
<outgoing uri="smtp+tls+://mail.espiv.net" username="$email" />
</provider>
<!-- Italy -->
<provider id="poste" label="poste" domain="poste.it">
<incoming uri="imap+ssl+://relay.poste.it" username="$email" />
<outgoing uri="smtp+ssl+://relay.poste.it" username="$email" />
</provider>
<provider id="vodafone" label="vodafone" domain="vodafone.it">
<incoming uri="imap+ssl+://imap.vodafone.it" username="$email" />
<outgoing uri="smtp+ssl+://smtp.vodafone.it" username="$email" />
</provider>
<!-- Switzerland -->
<!-- KolabNow.com variants -->
<provider id="kolabnow.com" label="KolabNow.com" domain="kolabnow.com">
<incoming uri="imap+ssl+://imap.kolabnow.com" username="$email" />
<outgoing uri="smtp+tls+://smtp.kolabnow.com" username="$email" />
</provider>
<provider id="attorneymail.ch" label="KolabNow.com" domain="attorneymail.ch">
<incoming uri="imap+ssl+://imap.kolabnow.com" username="$email" />
<outgoing uri="smtp+tls+://smtp.kolabnow.com" username="$email" />
</provider>
<provider id="barmail.ch" label="KolabNow.com" domain="barmail.ch">
<incoming uri="imap+ssl+://imap.kolabnow.com" username="$email" />
<outgoing uri="smtp+tls+://smtp.kolabnow.com" username="$email" />
</provider>
<provider id="collaborative.li" label="KolabNow.com" domain="collaborative.li">
<incoming uri="imap+ssl+://imap.kolabnow.com" username="$email" />
<outgoing uri="smtp+tls+://smtp.kolabnow.com" username="$email" />
</provider>
<provider id="diplomail.ch" label="KolabNow.com" domain="diplomail.ch">
<incoming uri="imap+ssl+://imap.kolabnow.com" username="$email" />
<outgoing uri="smtp+tls+://smtp.kolabnow.com" username="$email" />
</provider>
<provider id="groupoffice.ch" label="KolabNow.com" domain="groupoffice.ch">
<incoming uri="imap+ssl+://imap.kolabnow.com" username="$email" />
<outgoing uri="smtp+tls+://smtp.kolabnow.com" username="$email" />
</provider>
<provider id="journalistmail.ch" label="KolabNow.com" domain="journalistmail.ch">
<incoming uri="imap+ssl+://imap.kolabnow.com" username="$email" />
<outgoing uri="smtp+tls+://smtp.kolabnow.com" username="$email" />
</provider>
<provider id="legalprivilege.ch" label="KolabNow.com" domain="legalprivilege.ch">
<incoming uri="imap+ssl+://imap.kolabnow.com" username="$email" />
<outgoing uri="smtp+tls+://smtp.kolabnow.com" username="$email" />
</provider>
<provider id="libertymail.co" label="KolabNow.com" domain="libertymail.co">
<incoming uri="imap+ssl+://imap.kolabnow.com" username="$email" />
<outgoing uri="smtp+tls+://smtp.kolabnow.com" username="$email" />
</provider>
<provider id="libertymail.net" label="KolabNow.com" domain="libertymail.net">
<incoming uri="imap+ssl+://imap.kolabnow.com" username="$email" />
<outgoing uri="smtp+tls+://smtp.kolabnow.com" username="$email" />
</provider>
<provider id="mailatlaw.ch" label="KolabNow.com" domain="mailatlaw.ch">
<incoming uri="imap+ssl+://imap.kolabnow.com" username="$email" />
<outgoing uri="smtp+tls+://smtp.kolabnow.com" username="$email" />
</provider>
<provider id="medmail.ch" label="KolabNow.com" domain="medmail.ch">
<incoming uri="imap+ssl+://imap.kolabnow.com" username="$email" />
<outgoing uri="smtp+tls+://smtp.kolabnow.com" username="$email" />
</provider>
<provider id="mykolab.ch" label="KolabNow.com" domain="mykolab.ch">
<incoming uri="imap+ssl+://imap.kolabnow.com" username="$email" />
<outgoing uri="smtp+tls+://smtp.kolabnow.com" username="$email" />
</provider>
<provider id="mykolab.com" label="KolabNow.com" domain="mykolab.com">
<incoming uri="imap+ssl+://imap.kolabnow.com" username="$email" />
<outgoing uri="smtp+tls+://smtp.kolabnow.com" username="$email" />
</provider>
<provider id="myswissmail.ch" label="KolabNow.com" domain="myswissmail.ch">
<incoming uri="imap+ssl+://imap.kolabnow.com" username="$email" />
<outgoing uri="smtp+tls+://smtp.kolabnow.com" username="$email" />
</provider>
<provider id="opengroupware.ch" label="KolabNow.com" domain="opengroupware.ch">
<incoming uri="imap+ssl+://imap.kolabnow.com" username="$email" />
<outgoing uri="smtp+tls+://smtp.kolabnow.com" username="$email" />
</provider>
<provider id="pressmail.ch" label="KolabNow.com" domain="pressmail.ch">
<incoming uri="imap+ssl+://imap.kolabnow.com" username="$email" />
<outgoing uri="smtp+tls+://smtp.kolabnow.com" username="$email" />
</provider>
<provider id="swisscollab.ch" label="KolabNow.com" domain="swisscollab.ch">
<incoming uri="imap+ssl+://imap.kolabnow.com" username="$email" />
<outgoing uri="smtp+tls+://smtp.kolabnow.com" username="$email" />
</provider>
<provider id="swissgroupware.ch" label="KolabNow.com" domain="swissgroupware.ch">
<incoming uri="imap+ssl+://imap.kolabnow.com" username="$email" />
<outgoing uri="smtp+tls+://smtp.kolabnow.com" username="$email" />
</provider>
<provider id="switzerlandmail.ch" label="KolabNow.com" domain="switzerlandmail.ch">
<incoming uri="imap+ssl+://imap.kolabnow.com" username="$email" />
<outgoing uri="smtp+tls+://smtp.kolabnow.com" username="$email" />
</provider>
<provider id="trusted-legal-mail.ch" label="KolabNow.com" domain="trusted-legal-mail.ch">
<incoming uri="imap+ssl+://imap.kolabnow.com" username="$email" />
<outgoing uri="smtp+tls+://smtp.kolabnow.com" username="$email" />
</provider>
<!-- Japanese -->
<provider id="auone" label="au one" domain="auone.jp">
<incoming uri="imap+ssl+://imap.gmail.com" username="$email" />
<outgoing uri="smtp+ssl+://smtp.gmail.com" username="$email" />
</provider>
<!-- Korean -->
<provider id="naver" label="Naver" domain="naver.com">
<incoming uri="imap+ssl+://imap.naver.com" username="$user" />
<outgoing uri="smtp+tls+://smtp.naver.com:587" username="$user" />
</provider>
<provider id="hanmail" label="Hanmail" domain="hanmail.net">
<incoming uri="imap+ssl+://imap.hanmail.net" username="$user" />
<outgoing uri="smtp+ssl+://smtp.hanmail.net" username="$user" />
</provider>
<provider id="daum" label="Hanmail" domain="daum.net">
<incoming uri="imap+ssl+://imap.hanmail.net" username="$user" />
<outgoing uri="smtp+ssl+://smtp.hanmail.net" username="$user" />
</provider>
<!-- Russia -->
<!-- Mail.Ru variants -->
<provider id="rumailmailimap" label="mail.ru" domain="mail.ru">
<incoming uri="imap+ssl+://imap.mail.ru" username="$email" />
<outgoing uri="smtp+ssl+://smtp.mail.ru" username="$email" />
</provider>
<provider id="rumailinboximap" label="inbox.ru" domain="inbox.ru">
<incoming uri="imap+ssl+://imap.mail.ru" username="$email" />
<outgoing uri="smtp+ssl+://smtp.mail.ru" username="$email" />
</provider>
<provider id="rumaillistimap" label="list.ru" domain="list.ru">
<incoming uri="imap+ssl+://imap.mail.ru" username="$email" />
<outgoing uri="smtp+ssl+://smtp.mail.ru" username="$email" />
</provider>
<provider id="rumailbkimap" label="bk.ru" domain="bk.ru">
<incoming uri="imap+ssl+://imap.mail.ru" username="$email" />
<outgoing uri="smtp+ssl+://smtp.mail.ru" username="$email" />
</provider>
<!-- Yandex variants -->
<provider id="comyanyandeximap" label="yandex.com" domain="yandex.com">
<incoming uri="imap+ssl+://imap.yandex.com" username="$user" />
<outgoing uri="smtp+ssl+://smtp.yandex.com" username="$user" />
</provider>
<provider id="ruyanyandeximap" label="yandex.ru" domain="yandex.ru">
<incoming uri="imap+ssl+://imap.yandex.ru" username="$user" />
<outgoing uri="smtp+ssl+://smtp.yandex.ru" username="$user" />
</provider>
<provider id="ruyanyaimap" label="ya.ru" domain="ya.ru">
<incoming uri="imap+ssl+://imap.ya.ru" username="$user" />
<outgoing uri="smtp+ssl+://smtp.ya.ru" username="$user" />
</provider>
<provider id="byyandeximap" label="yandex.by" domain="yandex.by">
<incoming uri="imap+ssl+://imap.yandex.by" username="$user" />
<outgoing uri="smtp+ssl+://smtp.yandex.by" username="$user" />
</provider>
<provider id="kzyandeximap" label="yandex.kz" domain="yandex.kz">
<incoming uri="imap+ssl+://imap.yandex.kz" username="$user" />
<outgoing uri="smtp+ssl+://smtp.yandex.kz" username="$user" />
</provider>
<provider id="uayandeximap" label="yandex.ua" domain="yandex.ua">
<incoming uri="imap+ssl+://imap.yandex.ua" username="$user" />
<outgoing uri="smtp+ssl+://smtp.yandex.ua" username="$user" />
</provider>
<!-- Rambler.ru variants -->
<provider id="ruramramblerimap" label="rambler.ru" domain="rambler.ru">
<incoming uri="imap+ssl+://mail.rambler.ru" username="$email" />
<outgoing uri="smtp+ssl+://mail.rambler.ru" username="$email" />
</provider>
<provider id="ruramlentaimap" label="lenta.ru" domain="lenta.ru">
<incoming uri="imap+ssl+://mail.rambler.ru" username="$email" />
<outgoing uri="smtp+ssl+://mail.rambler.ru" username="$email" />
</provider>
<provider id="ruramroimap" label="ro.ru" domain="ro.ru">
<incoming uri="imap+ssl+://mail.rambler.ru" username="$email" />
<outgoing uri="smtp+ssl+://mail.rambler.ru" username="$email" />
</provider>
<!-- QIP.RU variants -->
<provider id="ruqipqipimap" label="qip.ru" domain="qip.ru">
<incoming uri="imap+ssl+://imap.qip.ru" username="$email" />
<outgoing uri="smtp+ssl+://smtp.qip.ru" username="$email" />
</provider>
<provider id="ruqippochtaimap" label="pochta.ru" domain="pochta.ru">
<incoming uri="imap+ssl+://imap.pochta.ru" username="$email" />
<outgoing uri="smtp+ssl+://smtp.pochta.ru" username="$email" />
</provider>
<provider id="comqipfromruimap" label="fromru.com" domain="fromru.com">
<incoming uri="imap+ssl+://imap.fromru.com" username="$email" />
<outgoing uri="smtp+ssl+://smtp.fromru.com" username="$email" />
</provider>
<provider id="ruqipfrontimap" label="front.ru" domain="front.ru">
<incoming uri="imap+ssl+://imap.front.ru" username="$email" />
<outgoing uri="smtp+ssl+://smtp.front.ru" username="$email" />
</provider>
<provider id="ruqiphotboximap" label="hotbox.ru" domain="hotbox.ru">
<incoming uri="imap+ssl+://imap.hotbox.ru" username="$email" />
<outgoing uri="smtp+ssl+://smtp.hotbox.ru" username="$email" />
</provider>
<provider id="ruqiphotmailimap" label="hotmail.ru" domain="hotmail.ru">
<incoming uri="imap+ssl+://imap.hotmail.ru" username="$email" />
<outgoing uri="smtp+ssl+://smtp.hotmail.ru" username="$email" />
</provider>
<provider id="suqipkrovatkaimap" label="krovatka.su" domain="krovatka.su">
<incoming uri="imap+ssl+://imap.krovatka.su" username="$email" />
<outgoing uri="smtp+ssl+://smtp.krovatka.su" username="$email" />
</provider>
<provider id="ruqiplandimap" label="land.ru" domain="land.ru">
<incoming uri="imap+ssl+://imap.land.ru" username="$email" />
<outgoing uri="smtp+ssl+://smtp.land.ru" username="$email" />
</provider>
<provider id="comqipmail15imap" label="mail15.com" domain="mail15.com">
<incoming uri="imap+ssl+://imap.mail15.com" username="$email" />
<outgoing uri="smtp+ssl+://smtp.mail15.com" username="$email" />
</provider>
<provider id="comqipmail333imap" label="mail333.com" domain="mail333.com">
<incoming uri="imap+ssl+://imap.mail333.com" username="$email" />
<outgoing uri="smtp+ssl+://smtp.mail333.com" username="$email" />
</provider>
<provider id="ruqipnewmailimap" label="newmail.ru" domain="newmail.ru">
<incoming uri="imap+ssl+://imap.newmail.ru" username="$email" />
<outgoing uri="smtp+ssl+://smtp.newmail.ru" username="$email" />
</provider>
<provider id="ruqipnightmailimap" label="nightmail.ru" domain="nightmail.ru">
<incoming uri="imap+ssl+://imap.nightmail.ru" username="$email" />
<outgoing uri="smtp+ssl+://smtp.nightmail.ru" username="$email" />
</provider>
<provider id="ruqipnmimap" label="nm.ru" domain="nm.ru">
<incoming uri="imap+ssl+://imap.nm.ru" username="$email" />
<outgoing uri="smtp+ssl+://smtp.nm.ru" username="$email" />
</provider>
<provider id="netqippisemimap" label="pisem.net" domain="pisem.net">
<incoming uri="imap+ssl+://imap.pisem.net" username="$email" />
<outgoing uri="smtp+ssl+://smtp.pisem.net" username="$email" />
</provider>
<provider id="ruqippochtamtimap" label="pochtamt.ru" domain="pochtamt.ru">
<incoming uri="imap+ssl+://imap.pochtamt.ru" username="$email" />
<outgoing uri="smtp+ssl+://smtp.pochtamt.ru" username="$email" />
</provider>
<provider id="ruqippop3imap" label="pop3.ru" domain="pop3.ru">
<incoming uri="imap+ssl+://imap.pop3.ru" username="$email" />
<outgoing uri="smtp+ssl+://smtp.pop3.ru" username="$email" />
</provider>
<provider id="ruqiprbcmailimap" label="rbcmail.ru" domain="rbcmail.ru">
<incoming uri="imap+ssl+://imap.rbcmail.ru" username="$email" />
<outgoing uri="smtp+ssl+://smtp.rbcmail.ru" username="$email" />
</provider>
<provider id="ruqipsmtpimap" label="smtp.ru" domain="smtp.ru">
<incoming uri="imap+ssl+://imap.smtp.ru" username="$email" />
<outgoing uri="smtp+ssl+://smtp.smtp.ru" username="$email" />
</provider>
<provider id="ruqip5ballovimap" label="5ballov.ru" domain="5ballov.ru">
<incoming uri="imap+ssl+://imap.5ballov.ru" username="$email" />
<outgoing uri="smtp+ssl+://smtp.5ballov.ru" username="$email" />
</provider>
<provider id="ruqipaeternaimap" label="aeterna.ru" domain="aeterna.ru">
<incoming uri="imap+ssl+://imap.aeterna.ru" username="$email" />
<outgoing uri="smtp+ssl+://smtp.aeterna.ru" username="$email" />
</provider>
<provider id="ruqipzizaimap" label="ziza.ru" domain="ziza.ru">
<incoming uri="imap+ssl+://imap.ziza.ru" username="$email" />
<outgoing uri="smtp+ssl+://smtp.ziza.ru" username="$email" />
</provider>
<provider id="ruqipmemoriimap" label="memori.ru" domain="memori.ru">
<incoming uri="imap+ssl+://imap.memori.ru" username="$email" />
<outgoing uri="smtp+ssl+://smtp.memori.ru" username="$email" />
</provider>
<provider id="ruqipphotofileimap" label="photofile.ru" domain="photofile.ru">
<incoming uri="imap+ssl+://imap.photofile.ru" username="$email" />
<outgoing uri="smtp+ssl+://smtp.photofile.ru" username="$email" />
</provider>
<provider id="ruqipfotoplenkaimap" label="fotoplenka.ru" domain="fotoplenka.ru">
<incoming uri="imap+ssl+://imap.fotoplenka.ru" username="$email" />
<outgoing uri="smtp+ssl+://smtp.fotoplenka.ru" username="$email" />
</provider>
<provider id="comqippochtaimap" label="pochta.com" domain="pochta.com">
<incoming uri="imap+ssl+://imap.pochta.ru" username="$email" />
<outgoing uri="smtp+ssl+://smtp.pochta.ru" username="$email" />
</provider>
<!-- Slovakia -->
<provider id="azet.sk" label="Azet.sk" domain="azet.sk">
<incoming uri="imap+ssl+://imap.azet.sk" username="$email" />
<outgoing uri="smtp+ssl+://smtp.azet.sk" username="$email" />
</provider>
<!-- The Netherlands -->
<!-- Ziggo variants -->
<provider id="casema.nl" label="Ziggo" domain="casema.nl">
<incoming uri="imap+ssl+://imap.ziggo.nl" username="$email" />
<outgoing uri="smtp+tls+://smtp.ziggo.nl" username="$email" />
</provider>
<provider id="chello.nl" label="Ziggo" domain="chello.nl">
<incoming uri="imap+ssl+://imap.ziggo.nl" username="$email" />
<outgoing uri="smtp+tls+://smtp.ziggo.nl" username="$email" />
</provider>
<provider id="hahah.nl" label="Ziggo" domain="hahah.nl">
<incoming uri="imap+ssl+://imap.ziggo.nl" username="$email" />
<outgoing uri="smtp+tls+://smtp.ziggo.nl" username="$email" />
</provider>
<provider id="home.nl" label="Ziggo" domain="home.nl">
<incoming uri="imap+ssl+://imap.ziggo.nl" username="$email" />
<outgoing uri="smtp+tls+://smtp.ziggo.nl" username="$email" />
</provider>
<provider id="multiweb.nl" label="Ziggo" domain="multiweb.nl">
<incoming uri="imap+ssl+://imap.ziggo.nl" username="$email" />
<outgoing uri="smtp+tls+://smtp.ziggo.nl" username="$email" />
</provider>
<provider id="quicknet.nl" label="Ziggo" domain="quicknet.nl">
<incoming uri="imap+ssl+://imap.ziggo.nl" username="$email" />
<outgoing uri="smtp+tls+://smtp.ziggo.nl" username="$email" />
</provider>
<provider id="razcall.com" label="Ziggo" domain="razcall.com">
<incoming uri="imap+ssl+://imap.ziggo.nl" username="$email" />
<outgoing uri="smtp+tls+://smtp.ziggo.nl" username="$email" />
</provider>
<provider id="razcall.nl" label="Ziggo" domain="razcall.nl">
<incoming uri="imap+ssl+://imap.ziggo.nl" username="$email" />
<outgoing uri="smtp+tls+://smtp.ziggo.nl" username="$email" />
</provider>
<provider id="upcmail.nl" label="Ziggo" domain="upcmail.nl">
<incoming uri="imap+ssl+://imap.ziggo.nl" username="$email" />
<outgoing uri="smtp+tls+://smtp.ziggo.nl" username="$email" />
</provider>
<provider id="zeggis.com" label="Ziggo" domain="zeggis.com">
<incoming uri="imap+ssl+://imap.ziggo.nl" username="$email" />
<outgoing uri="smtp+tls+://smtp.ziggo.nl" username="$email" />
</provider>
<provider id="zeggis.nl" label="Ziggo" domain="zeggis.nl">
<incoming uri="imap+ssl+://imap.ziggo.nl" username="$email" />
<outgoing uri="smtp+tls+://smtp.ziggo.nl" username="$email" />
</provider>
<provider id="ziggomail.com" label="Ziggo" domain="ziggomail.com">
<incoming uri="imap+ssl+://imap.ziggo.nl" username="$email" />
<outgoing uri="smtp+tls+://smtp.ziggo.nl" username="$email" />
</provider>
<provider id="ziggo.nl" label="Ziggo" domain="ziggo.nl">
<incoming uri="imap+ssl+://imap.ziggo.nl" username="$email" />
<outgoing uri="smtp+tls+://smtp.ziggo.nl" username="$email" />
</provider>
<provider id="zinders.nl" label="Ziggo" domain="zinders.nl">
<incoming uri="imap+ssl+://imap.ziggo.nl" username="$email" />
<outgoing uri="smtp+tls+://smtp.ziggo.nl" username="$email" />
</provider>
<!-- EU wide -->
<provider id="fairnatics.net" label="Fairnatics" domain="fairnatics.net">
<incoming uri="imap+ssl+://mail.fairnatics.net" username="$email" />
<outgoing uri="smtp+tls+://mail.fairnatics.net:25" username="$email" />
</provider>
<!-- eFoundation -->
<provider id="e.foundation" label="/e/" domain="e.email">
<incoming uri="imap+ssl+://mail.ecloud.global" username="$email" />
<outgoing uri="smtp+tls+://mail.ecloud.global" username="$email" />
</provider>
<!-- AOL variants -->
<provider domain="aol.com">
<incoming uri="imap+ssl+://imap.aol.com" username="$email" />
<outgoing uri="smtp+ssl+://smtp.aol.com" username="$email" />
</provider>
<provider domain="aol.de">
<incoming uri="imap+ssl+://imap.aol.com" username="$email" />
<outgoing uri="smtp+ssl+://smtp.aol.com" username="$email" />
</provider>
<provider domain="aol.it">
<incoming uri="imap+ssl+://imap.aol.com" username="$email" />
<outgoing uri="smtp+ssl+://smtp.aol.com" username="$email" />
</provider>
<provider domain="aol.fr">
<incoming uri="imap+ssl+://imap.aol.com" username="$email" />
<outgoing uri="smtp+ssl+://smtp.aol.com" username="$email" />
</provider>
<provider domain="aol.es">
<incoming uri="imap+ssl+://imap.aol.com" username="$email" />
<outgoing uri="smtp+ssl+://smtp.aol.com" username="$email" />
</provider>
<provider domain="aol.se">
<incoming uri="imap+ssl+://imap.aol.com" username="$email" />
<outgoing uri="smtp+ssl+://smtp.aol.com" username="$email" />
</provider>
<provider domain="aol.co.uk">
<incoming uri="imap+ssl+://imap.aol.com" username="$email" />
<outgoing uri="smtp+ssl+://smtp.aol.com" username="$email" />
</provider>
<provider domain="aol.co.nz">
<incoming uri="imap+ssl+://imap.aol.com" username="$email" />
<outgoing uri="smtp+ssl+://smtp.aol.com" username="$email" />
</provider>
<provider domain="aol.com.au">
<incoming uri="imap+ssl+://imap.aol.com" username="$email" />
<outgoing uri="smtp+ssl+://smtp.aol.com" username="$email" />
</provider>
<provider domain="aol.com.ar">
<incoming uri="imap+ssl+://imap.aol.com" username="$email" />
<outgoing uri="smtp+ssl+://smtp.aol.com" username="$email" />
</provider>
<provider domain="aol.com.br">
<incoming uri="imap+ssl+://imap.aol.com" username="$email" />
<outgoing uri="smtp+ssl+://smtp.aol.com" username="$email" />
</provider>
<provider domain="aol.com.mx">
<incoming uri="imap+ssl+://imap.aol.com" username="$email" />
<outgoing uri="smtp+ssl+://smtp.aol.com" username="$email" />
</provider>
<!-- Microsoft variants -->
<provider domain="outlook.com">
<incoming uri="imap+ssl+://outlook.office365.com" username="$email" />
<outgoing uri="smtp+tls+://smtp.office365.com" username="$email" />
</provider>
<provider domain="hotmail.com">
<incoming uri="imap+ssl+://outlook.office365.com" username="$email" />
<outgoing uri="smtp+tls+://smtp.office365.com" username="$email" />
</provider>
<provider domain="msn.com">
<incoming uri="imap+ssl+://outlook.office365.com" username="$email" />
<outgoing uri="smtp+tls+://smtp.office365.com" username="$email" />
</provider>
<provider domain="live.com">
<incoming uri="imap+ssl+://outlook.office365.com" username="$email" />
<outgoing uri="smtp+tls+://smtp.office365.com" username="$email" />
</provider>
<provider domain="live.co.uk">
<incoming uri="imap+ssl+://outlook.office365.com" username="$email" />
<outgoing uri="smtp+tls+://smtp.office365.com" username="$email" />
</provider>
<provider domain="hotmail.co.uk">
<incoming uri="imap+ssl+://outlook.office365.com" username="$email" />
<outgoing uri="smtp+tls+://smtp.office365.com" username="$email" />
</provider>
<provider domain="outlook.sk">
<incoming uri="imap+ssl+://outlook.office365.com" username="$email" />
<outgoing uri="smtp+tls+://smtp.office365.com" username="$email" />
</provider>
</providers>

View file

@ -0,0 +1,64 @@
package com.fsck.k9.autodiscovery.providersxml
import androidx.test.core.app.ApplicationProvider
import assertk.assertThat
import assertk.assertions.isEqualTo
import assertk.assertions.isNotNull
import assertk.assertions.isNull
import com.fsck.k9.RobolectricTest
import com.fsck.k9.mail.AuthType
import com.fsck.k9.mail.ConnectionSecurity
import com.fsck.k9.oauth.OAuthConfiguration
import com.fsck.k9.oauth.OAuthConfigurationProvider
import org.junit.Test
class ProvidersXmlDiscoveryTest : RobolectricTest() {
private val xmlProvider = ProvidersXmlProvider(ApplicationProvider.getApplicationContext())
private val oAuthConfigurationProvider = createOAuthConfigurationProvider()
private val providersXmlDiscovery = ProvidersXmlDiscovery(xmlProvider, oAuthConfigurationProvider)
@Test
fun discover_withGmailDomain_shouldReturnCorrectSettings() {
val connectionSettings = providersXmlDiscovery.discover("user@gmail.com")
assertThat(connectionSettings).isNotNull()
with(connectionSettings!!.incoming.first()) {
assertThat(host).isEqualTo("imap.gmail.com")
assertThat(security).isEqualTo(ConnectionSecurity.SSL_TLS_REQUIRED)
assertThat(authType).isEqualTo(AuthType.XOAUTH2)
assertThat(username).isEqualTo("user@gmail.com")
}
with(connectionSettings.outgoing.first()) {
assertThat(host).isEqualTo("smtp.gmail.com")
assertThat(security).isEqualTo(ConnectionSecurity.SSL_TLS_REQUIRED)
assertThat(authType).isEqualTo(AuthType.XOAUTH2)
assertThat(username).isEqualTo("user@gmail.com")
}
}
@Test
fun discover_withUnknownDomain_shouldReturnNull() {
val connectionSettings = providersXmlDiscovery.discover(
"user@not.present.in.providers.xml.example"
)
assertThat(connectionSettings).isNull()
}
private fun createOAuthConfigurationProvider(): OAuthConfigurationProvider {
val googleConfig = OAuthConfiguration(
clientId = "irrelevant",
scopes = listOf("irrelevant"),
authorizationEndpoint = "irrelevant",
tokenEndpoint = "irrelevant",
redirectUri = "irrelevant"
)
return OAuthConfigurationProvider(
configurations = mapOf(
listOf("imap.gmail.com", "smtp.gmail.com") to googleConfig
),
googleConfiguration = googleConfig
)
}
}

View file

@ -0,0 +1,11 @@
@Suppress("DSL_SCOPE_VIOLATION")
plugins {
id(ThunderbirdPlugins.Library.jvm)
alias(libs.plugins.android.lint)
}
dependencies {
api(projects.app.autodiscovery.api)
implementation(libs.minidns.hla)
}

View file

@ -0,0 +1,29 @@
package com.fsck.k9.autodiscovery.srvrecords
import com.fsck.k9.mail.ConnectionSecurity.SSL_TLS_REQUIRED
import com.fsck.k9.mail.ConnectionSecurity.STARTTLS_REQUIRED
import org.minidns.dnslabel.DnsLabel
import org.minidns.dnsname.DnsName
import org.minidns.hla.ResolverApi
import org.minidns.hla.srv.SrvProto
class MiniDnsSrvResolver : SrvResolver {
override fun lookup(domain: String, type: SrvType): List<MailService> {
val result = ResolverApi.INSTANCE.resolveSrv(
DnsLabel.from(type.label),
SrvProto.tcp.dnsLabel,
DnsName.from(domain)
)
val security = if (type.assumeTls) SSL_TLS_REQUIRED else STARTTLS_REQUIRED
return result.answersOrEmptySet.map {
MailService(
srvType = type,
host = it.target.toString(),
port = it.port,
priority = it.priority,
security = security
)
}
}
}

View file

@ -0,0 +1,5 @@
package com.fsck.k9.autodiscovery.srvrecords
interface SrvResolver {
fun lookup(domain: String, type: SrvType): List<MailService>
}

View file

@ -0,0 +1,56 @@
package com.fsck.k9.autodiscovery.srvrecords
import com.fsck.k9.autodiscovery.api.ConnectionSettingsDiscovery
import com.fsck.k9.autodiscovery.api.DiscoveredServerSettings
import com.fsck.k9.autodiscovery.api.DiscoveryResults
import com.fsck.k9.helper.EmailHelper
import com.fsck.k9.mail.AuthType
import com.fsck.k9.mail.ConnectionSecurity
class SrvServiceDiscovery(
private val srvResolver: MiniDnsSrvResolver
) : ConnectionSettingsDiscovery {
override fun discover(email: String): DiscoveryResults? {
val domain = EmailHelper.getDomainFromEmailAddress(email) ?: return null
val mailServicePriority = compareBy<MailService> { it.priority }.thenByDescending { it.security }
val outgoingSettings = listOf(SrvType.SUBMISSIONS, SrvType.SUBMISSION)
.flatMap { srvResolver.lookup(domain, it) }
.sortedWith(mailServicePriority)
.map { newServerSettings(it, email) }
val incomingSettings = listOf(SrvType.IMAPS, SrvType.IMAP)
.flatMap { srvResolver.lookup(domain, it) }
.sortedWith(mailServicePriority)
.map { newServerSettings(it, email) }
return DiscoveryResults(incoming = incomingSettings, outgoing = outgoingSettings)
}
}
fun newServerSettings(service: MailService, email: String): DiscoveredServerSettings {
return DiscoveredServerSettings(
service.srvType.protocol,
service.host,
service.port,
service.security,
AuthType.PLAIN,
email
)
}
enum class SrvType(val label: String, val protocol: String, val assumeTls: Boolean) {
SUBMISSIONS("_submissions", "smtp", true),
SUBMISSION("_submission", "smtp", false),
IMAPS("_imaps", "imap", true),
IMAP("_imap", "imap", false)
}
data class MailService(
val srvType: SrvType,
val host: String,
val port: Int,
val priority: Int,
val security: ConnectionSecurity
)

View file

@ -0,0 +1,178 @@
package com.fsck.k9.autodiscovery.srvrecords
import com.fsck.k9.autodiscovery.api.DiscoveryResults
import com.fsck.k9.mail.ConnectionSecurity
import org.junit.Assert.assertEquals
import org.junit.Test
import org.mockito.kotlin.doReturn
import org.mockito.kotlin.mock
class SrvServiceDiscoveryTest {
@Test
fun discover_whenNoMailServices_shouldReturnNoResults() {
val srvResolver = newMockSrvResolver()
val srvServiceDiscovery = SrvServiceDiscovery(srvResolver)
val result = srvServiceDiscovery.discover("test@example.com")
assertEquals(DiscoveryResults(listOf(), listOf()), result)
}
@Test
fun discover_whenNoSMTP_shouldReturnJustIMAP() {
val srvResolver = newMockSrvResolver(
imapServices = listOf(newMailService(port = 143, srvType = SrvType.IMAP)),
imapsServices = listOf(
newMailService(port = 993, srvType = SrvType.IMAPS, security = ConnectionSecurity.SSL_TLS_REQUIRED)
)
)
val srvServiceDiscovery = SrvServiceDiscovery(srvResolver)
val result = srvServiceDiscovery.discover("test@example.com")
assertEquals(2, result!!.incoming.size)
assertEquals(0, result.outgoing.size)
}
@Test
fun discover_whenNoIMAP_shouldReturnJustSMTP() {
val srvResolver = newMockSrvResolver(
submissionServices = listOf(
newMailService(
port = 25,
srvType = SrvType.SUBMISSION,
security = ConnectionSecurity.STARTTLS_REQUIRED
),
newMailService(
port = 465,
srvType = SrvType.SUBMISSIONS,
security = ConnectionSecurity.SSL_TLS_REQUIRED
)
)
)
val srvServiceDiscovery = SrvServiceDiscovery(srvResolver)
val result = srvServiceDiscovery.discover("test@example.com")
assertEquals(0, result!!.incoming.size)
assertEquals(2, result.outgoing.size)
}
@Test
fun discover_withRequiredServices_shouldCorrectlyPrioritize() {
val srvResolver = newMockSrvResolver(
submissionServices = listOf(
newMailService(
host = "smtp1.example.com",
port = 25,
srvType = SrvType.SUBMISSION,
security = ConnectionSecurity.STARTTLS_REQUIRED,
priority = 0
),
newMailService(
host = "smtp2.example.com",
port = 25,
srvType = SrvType.SUBMISSION,
security = ConnectionSecurity.STARTTLS_REQUIRED,
priority = 1
)
),
submissionsServices = listOf(
newMailService(
host = "smtp3.example.com",
port = 465,
srvType = SrvType.SUBMISSIONS,
security = ConnectionSecurity.SSL_TLS_REQUIRED,
priority = 0
),
newMailService(
host = "smtp4.example.com",
port = 465,
srvType = SrvType.SUBMISSIONS,
security = ConnectionSecurity.SSL_TLS_REQUIRED,
priority = 1
)
),
imapServices = listOf(
newMailService(
host = "imap1.example.com",
port = 143,
srvType = SrvType.IMAP,
security = ConnectionSecurity.STARTTLS_REQUIRED,
priority = 0
),
newMailService(
host = "imap2.example.com",
port = 143,
srvType = SrvType.IMAP,
security = ConnectionSecurity.STARTTLS_REQUIRED,
priority = 1
)
),
imapsServices = listOf(
newMailService(
host = "imaps1.example.com",
port = 993,
srvType = SrvType.IMAPS,
security = ConnectionSecurity.SSL_TLS_REQUIRED,
priority = 0
),
newMailService(
host = "imaps2.example.com",
port = 993,
srvType = SrvType.IMAPS,
security = ConnectionSecurity.SSL_TLS_REQUIRED,
priority = 1
)
)
)
val srvServiceDiscovery = SrvServiceDiscovery(srvResolver)
val result = srvServiceDiscovery.discover("test@example.com")
assertEquals(
listOf(
"smtp3.example.com",
"smtp1.example.com",
"smtp4.example.com",
"smtp2.example.com"
),
result?.outgoing?.map { it.host }
)
assertEquals(
listOf(
"imaps1.example.com",
"imap1.example.com",
"imaps2.example.com",
"imap2.example.com"
),
result?.incoming?.map { it.host }
)
}
private fun newMailService(
host: String = "example.com",
priority: Int = 0,
security: ConnectionSecurity = ConnectionSecurity.STARTTLS_REQUIRED,
srvType: SrvType,
port: Int
): MailService {
return MailService(srvType, host, port, priority, security)
}
private fun newMockSrvResolver(
host: String = "example.com",
submissionServices: List<MailService> = listOf(),
submissionsServices: List<MailService> = listOf(),
imapServices: List<MailService> = listOf(),
imapsServices: List<MailService> = listOf()
): MiniDnsSrvResolver {
return mock {
on { lookup(host, SrvType.SUBMISSION) } doReturn submissionServices
on { lookup(host, SrvType.SUBMISSIONS) } doReturn submissionsServices
on { lookup(host, SrvType.IMAP) } doReturn imapServices
on { lookup(host, SrvType.IMAPS) } doReturn imapsServices
}
}
}

View file

@ -0,0 +1,15 @@
@Suppress("DSL_SCOPE_VIOLATION")
plugins {
id(ThunderbirdPlugins.Library.jvm)
alias(libs.plugins.android.lint)
}
dependencies {
api(projects.app.autodiscovery.api)
compileOnly(libs.xmlpull)
implementation(libs.okhttp)
testImplementation(libs.kxml2)
testImplementation(libs.okhttp.mockwebserver)
}

View file

@ -0,0 +1,28 @@
package com.fsck.k9.autodiscovery.thunderbird
import com.fsck.k9.logging.Timber
import java.io.IOException
import java.io.InputStream
import okhttp3.HttpUrl
import okhttp3.OkHttpClient
import okhttp3.Request
class ThunderbirdAutoconfigFetcher(private val okHttpClient: OkHttpClient) {
fun fetchAutoconfigFile(url: HttpUrl): InputStream? {
return try {
val request = Request.Builder().url(url).build()
val response = okHttpClient.newCall(request).execute()
if (response.isSuccessful) {
response.body?.byteStream()
} else {
null
}
} catch (e: IOException) {
Timber.d(e, "Error fetching URL: %s", url)
null
}
}
}

View file

@ -0,0 +1,102 @@
package com.fsck.k9.autodiscovery.thunderbird
import com.fsck.k9.autodiscovery.api.DiscoveredServerSettings
import com.fsck.k9.autodiscovery.api.DiscoveryResults
import com.fsck.k9.mail.AuthType
import com.fsck.k9.mail.ConnectionSecurity
import java.io.IOException
import java.io.InputStream
import java.io.InputStreamReader
import org.xmlpull.v1.XmlPullParser
import org.xmlpull.v1.XmlPullParserException
import org.xmlpull.v1.XmlPullParserFactory
/**
* Parser for Thunderbird's
* [Autoconfig file format](https://wiki.mozilla.org/Thunderbird:Autoconfiguration:ConfigFileFormat)
*/
class ThunderbirdAutoconfigParser {
fun parseSettings(stream: InputStream, email: String): DiscoveryResults? {
val factory = XmlPullParserFactory.newInstance()
val xpp = factory.newPullParser()
xpp.setInput(InputStreamReader(stream))
val incomingServers = mutableListOf<DiscoveredServerSettings>()
val outgoingServers = mutableListOf<DiscoveredServerSettings>()
var eventType = xpp.eventType
while (eventType != XmlPullParser.END_DOCUMENT) {
if (eventType == XmlPullParser.START_TAG) {
when (xpp.name) {
"incomingServer" -> {
incomingServers += parseServer(xpp, "incomingServer", email)
}
"outgoingServer" -> {
outgoingServers += parseServer(xpp, "outgoingServer", email)
}
}
}
eventType = xpp.next()
}
return DiscoveryResults(incomingServers, outgoingServers)
}
private fun parseServer(xpp: XmlPullParser, nodeName: String, email: String): DiscoveredServerSettings {
val type = xpp.getAttributeValue(null, "type")
var host: String? = null
var username: String? = null
var port: Int? = null
var authType: AuthType? = null
var connectionSecurity: ConnectionSecurity? = null
var eventType = xpp.eventType
while (!(eventType == XmlPullParser.END_TAG && nodeName == xpp.name)) {
if (eventType == XmlPullParser.START_TAG) {
when (xpp.name) {
"hostname" -> {
host = getText(xpp)
}
"port" -> {
port = getText(xpp).toInt()
}
"username" -> {
username = getText(xpp).replace("%EMAILADDRESS%", email)
}
"authentication" -> {
if (authType == null) authType = parseAuthType(getText(xpp))
}
"socketType" -> {
connectionSecurity = parseSocketType(getText(xpp))
}
}
}
eventType = xpp.next()
}
return DiscoveredServerSettings(type, host!!, port!!, connectionSecurity!!, authType, username)
}
private fun parseAuthType(authentication: String): AuthType? {
return when (authentication) {
"password-cleartext" -> AuthType.PLAIN
"TLS-client-cert" -> AuthType.EXTERNAL
"secure" -> AuthType.CRAM_MD5
else -> null
}
}
private fun parseSocketType(socketType: String): ConnectionSecurity? {
return when (socketType) {
"plain" -> ConnectionSecurity.NONE
"SSL" -> ConnectionSecurity.SSL_TLS_REQUIRED
"STARTTLS" -> ConnectionSecurity.STARTTLS_REQUIRED
else -> null
}
}
@Throws(XmlPullParserException::class, IOException::class)
private fun getText(xpp: XmlPullParser): String {
val eventType = xpp.next()
return if (eventType != XmlPullParser.TEXT) "" else xpp.text
}
}

View file

@ -0,0 +1,47 @@
package com.fsck.k9.autodiscovery.thunderbird
import com.fsck.k9.helper.EmailHelper
import okhttp3.HttpUrl
import okhttp3.HttpUrl.Companion.toHttpUrl
class ThunderbirdAutoconfigUrlProvider {
fun getAutoconfigUrls(email: String): List<HttpUrl> {
val domain = EmailHelper.getDomainFromEmailAddress(email)
requireNotNull(domain) { "Couldn't extract domain from email address: $email" }
return listOf(
createProviderUrl(domain, email),
createDomainUrl(scheme = "https", domain),
createDomainUrl(scheme = "http", domain),
createIspDbUrl(domain)
)
}
private fun createProviderUrl(domain: String?, email: String): HttpUrl {
// https://autoconfig.{domain}/mail/config-v1.1.xml?emailaddress={email}
return HttpUrl.Builder()
.scheme("https")
.host("autoconfig.$domain")
.addEncodedPathSegments("mail/config-v1.1.xml")
.addQueryParameter("emailaddress", email)
.build()
}
private fun createDomainUrl(scheme: String, domain: String): HttpUrl {
// https://{domain}/.well-known/autoconfig/mail/config-v1.1.xml
// http://{domain}/.well-known/autoconfig/mail/config-v1.1.xml
return HttpUrl.Builder()
.scheme(scheme)
.host(domain)
.addEncodedPathSegments(".well-known/autoconfig/mail/config-v1.1.xml")
.build()
}
private fun createIspDbUrl(domain: String): HttpUrl {
// https://autoconfig.thunderbird.net/v1.1/{domain}
return "https://autoconfig.thunderbird.net/v1.1/".toHttpUrl()
.newBuilder()
.addPathSegment(domain)
.build()
}
}

View file

@ -0,0 +1,28 @@
package com.fsck.k9.autodiscovery.thunderbird
import com.fsck.k9.autodiscovery.api.ConnectionSettingsDiscovery
import com.fsck.k9.autodiscovery.api.DiscoveryResults
class ThunderbirdDiscovery(
private val urlProvider: ThunderbirdAutoconfigUrlProvider,
private val fetcher: ThunderbirdAutoconfigFetcher,
private val parser: ThunderbirdAutoconfigParser
) : ConnectionSettingsDiscovery {
override fun discover(email: String): DiscoveryResults? {
val autoconfigUrls = urlProvider.getAutoconfigUrls(email)
return autoconfigUrls
.asSequence()
.mapNotNull { autoconfigUrl ->
fetcher.fetchAutoconfigFile(autoconfigUrl)?.use { inputStream ->
parser.parseSettings(inputStream, email)
}
}
.firstOrNull { result ->
result.incoming.isNotEmpty() || result.outgoing.isNotEmpty()
}
}
override fun toString(): String = "Thunderbird autoconfig"
}

View file

@ -0,0 +1,44 @@
package com.fsck.k9.autodiscovery.thunderbird
import assertk.assertThat
import assertk.assertions.isEqualTo
import assertk.assertions.isNull
import kotlin.test.assertNotNull
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.OkHttpClient
import okhttp3.mockwebserver.MockResponse
import okhttp3.mockwebserver.MockWebServer
import org.junit.Test
class ThunderbirdAutoconfigFetcherTest {
private val fetcher = ThunderbirdAutoconfigFetcher(OkHttpClient.Builder().build())
@Test
fun shouldHandleNonexistentUrl() {
val nonExistentUrl =
"https://autoconfig.domain.invalid/mail/config-v1.1.xml?emailaddress=test%40domain.example".toHttpUrl()
val inputStream = fetcher.fetchAutoconfigFile(nonExistentUrl)
assertThat(inputStream).isNull()
}
@Test
fun shouldHandleEmptyResponse() {
val server = MockWebServer().apply {
this.enqueue(
MockResponse()
.setBody("")
.setResponseCode(204),
)
start()
}
val url = server.url("/empty/")
val inputStream = fetcher.fetchAutoconfigFile(url)
assertNotNull(inputStream) { inputStream ->
assertThat(inputStream.available()).isEqualTo(0)
}
}
}

View file

@ -0,0 +1,174 @@
package com.fsck.k9.autodiscovery.thunderbird
import assertk.assertThat
import assertk.assertions.isEqualTo
import assertk.assertions.isNotNull
import com.fsck.k9.autodiscovery.api.DiscoveredServerSettings
import com.fsck.k9.autodiscovery.api.DiscoveryResults
import com.fsck.k9.mail.AuthType
import com.fsck.k9.mail.ConnectionSecurity
import org.junit.Test
class ThunderbirdAutoconfigTest {
private val parser = ThunderbirdAutoconfigParser()
@Test
fun settingsExtract() {
val input =
"""
<?xml version="1.0"?>
<clientConfig version="1.1">
<emailProvider id="metacode.biz">
<domain>metacode.biz</domain>
<incomingServer type="imap">
<hostname>imap.googlemail.com</hostname>
<port>993</port>
<socketType>SSL</socketType>
<username>%EMAILADDRESS%</username>
<authentication>OAuth2</authentication>
<authentication>password-cleartext</authentication>
</incomingServer>
<outgoingServer type="smtp">
<hostname>smtp.googlemail.com</hostname>
<port>465</port>
<socketType>SSL</socketType>
<username>%EMAILADDRESS%</username>
<authentication>OAuth2</authentication>
<authentication>password-cleartext</authentication>
<addThisServer>true</addThisServer>
</outgoingServer>
</emailProvider>
</clientConfig>
""".trimIndent().byteInputStream()
val connectionSettings = parser.parseSettings(input, "test@metacode.biz")
assertThat(connectionSettings).isNotNull()
assertThat(connectionSettings!!.incoming).isNotNull()
assertThat(connectionSettings.outgoing).isNotNull()
with(connectionSettings.incoming.first()) {
assertThat(host).isEqualTo("imap.googlemail.com")
assertThat(port).isEqualTo(993)
assertThat(username).isEqualTo("test@metacode.biz")
}
with(connectionSettings.outgoing.first()) {
assertThat(host).isEqualTo("smtp.googlemail.com")
assertThat(port).isEqualTo(465)
assertThat(username).isEqualTo("test@metacode.biz")
}
}
@Test
fun multipleServers() {
val input =
"""
<?xml version="1.0"?>
<clientConfig version="1.1">
<emailProvider id="metacode.biz">
<domain>metacode.biz</domain>
<incomingServer type="imap">
<hostname>imap.googlemail.com</hostname>
<port>993</port>
<socketType>SSL</socketType>
<username>%EMAILADDRESS%</username>
<authentication>OAuth2</authentication>
<authentication>password-cleartext</authentication>
</incomingServer>
<outgoingServer type="smtp">
<hostname>first</hostname>
<port>465</port>
<socketType>SSL</socketType>
<username>%EMAILADDRESS%</username>
<authentication>OAuth2</authentication>
<authentication>password-cleartext</authentication>
<addThisServer>true</addThisServer>
</outgoingServer>
<outgoingServer type="smtp">
<hostname>second</hostname>
<port>465</port>
<socketType>SSL</socketType>
<username>%EMAILADDRESS%</username>
<authentication>OAuth2</authentication>
<authentication>password-cleartext</authentication>
<addThisServer>true</addThisServer>
</outgoingServer>
</emailProvider>
</clientConfig>
""".trimIndent().byteInputStream()
val discoveryResults = parser.parseSettings(input, "test@metacode.biz")
assertThat(discoveryResults).isNotNull()
assertThat(discoveryResults!!.outgoing).isNotNull()
with(discoveryResults.outgoing[0]) {
assertThat(host).isEqualTo("first")
assertThat(port).isEqualTo(465)
assertThat(username).isEqualTo("test@metacode.biz")
}
with(discoveryResults.outgoing[1]) {
assertThat(host).isEqualTo("second")
assertThat(port).isEqualTo(465)
assertThat(username).isEqualTo("test@metacode.biz")
}
}
@Test
fun invalidResponse() {
val input =
"""
<?xml version="1.0"?>
<clientConfig version="1.1">
<emailProvider id="metacode.biz">
<domain>metacode.biz</domain>
""".trimIndent().byteInputStream()
val connectionSettings = parser.parseSettings(input, "test@metacode.biz")
assertThat(connectionSettings).isEqualTo(DiscoveryResults(listOf(), listOf()))
}
@Test
fun incompleteConfiguration() {
val input =
"""
<?xml version="1.0"?>
<clientConfig version="1.1">
<emailProvider id="metacode.biz">
<domain>metacode.biz</domain>
<incomingServer type="imap">
<hostname>imap.googlemail.com</hostname>
<port>993</port>
<socketType>SSL</socketType>
<username>%EMAILADDRESS%</username>
<authentication>OAuth2</authentication>
<authentication>password-cleartext</authentication>
</incomingServer>
</emailProvider>
</clientConfig>
""".trimIndent().byteInputStream()
val connectionSettings = parser.parseSettings(input, "test@metacode.biz")
assertThat(connectionSettings).isEqualTo(
DiscoveryResults(
listOf(
DiscoveredServerSettings(
protocol = "imap",
host = "imap.googlemail.com",
port = 993,
security = ConnectionSecurity.SSL_TLS_REQUIRED,
authType = AuthType.PLAIN,
username = "test@metacode.biz"
)
),
listOf()
)
)
}
}

View file

@ -0,0 +1,21 @@
package com.fsck.k9.autodiscovery.thunderbird
import assertk.assertThat
import assertk.assertions.containsExactly
import org.junit.Test
class ThunderbirdAutoconfigUrlProviderTest {
private val urlProvider = ThunderbirdAutoconfigUrlProvider()
@Test
fun `getAutoconfigUrls with ASCII email address`() {
val autoconfigUrls = urlProvider.getAutoconfigUrls("test@domain.example")
assertThat(autoconfigUrls.map { it.toString() }).containsExactly(
"https://autoconfig.domain.example/mail/config-v1.1.xml?emailaddress=test%40domain.example",
"https://domain.example/.well-known/autoconfig/mail/config-v1.1.xml",
"http://domain.example/.well-known/autoconfig/mail/config-v1.1.xml",
"https://autoconfig.thunderbird.net/v1.1/domain.example"
)
}
}

51
app/core/build.gradle.kts Normal file
View 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
}
}

View 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>

View 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
}
}

View file

@ -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"
}
}

View file

@ -0,0 +1,5 @@
package com.fsck.k9
fun interface AccountRemovedListener {
fun onAccountRemoved(account: Account)
}

View file

@ -0,0 +1,6 @@
package com.fsck.k9;
public interface AccountsChangeListener {
void onAccountsChanged();
}

View 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()
}

View file

@ -0,0 +1,5 @@
package com.fsck.k9
data class AppConfig(
val componentsToDisable: List<Class<*>>
)

View file

@ -0,0 +1,7 @@
package com.fsck.k9
interface BaseAccount {
val uuid: String
val name: String?
val email: String
}

View 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)
}
}
}

View 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
)

View 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
}

View 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) }

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

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

View 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)
}

View 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
}
}

View 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() }
}

View 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)
}
}
}

View 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()
}

View 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
)

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

View 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()
}
}
}

View 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;
}
}
}

View 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()
}
}

View 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()
}

View 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)
}

Some files were not shown because too many files have changed in this diff Show more