Repo created
15
.editorconfig
Normal 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
|
|
@ -0,0 +1,4 @@
|
|||
* text=auto eol=lf
|
||||
|
||||
*.bat text eol=crlf
|
||||
*.jar binary
|
||||
32
.gitignore
vendored
Normal 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
|
||||
# don’t 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
|
|
@ -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
|
|
@ -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
|
|
@ -0,0 +1,3 @@
|
|||
K-9 Mail
|
||||
Copyright 2008-2016, K-9 Mail Developers
|
||||
Copyright 2005-2016, The Android Open Source Project
|
||||
69
README.md
|
|
@ -1,3 +1,68 @@
|
|||
# mail
|
||||
# K-9 Mail
|
||||
|
||||
Monocles E-Mail Client for Android
|
||||
[](https://github.com/thundernest/k-9/releases/latest)
|
||||
[](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
|
|
@ -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.
|
||||
21
app-ui-catalog/build.gradle.kts
Normal 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
|
|
@ -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
|
||||
23
app-ui-catalog/src/main/AndroidManifest.xml
Normal 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>
|
||||
BIN
app-ui-catalog/src/main/ic_launcher-playstore.png
Normal file
|
After Width: | Height: | Size: 21 KiB |
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
package app.k9mail.ui.catalog
|
||||
|
||||
enum class CatalogThemeVariant {
|
||||
LIGHT, DARK
|
||||
}
|
||||
|
|
@ -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() },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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") }
|
||||
}
|
||||
|
|
@ -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>
|
||||
104
app-ui-catalog/src/main/res/drawable/ic_launcher_foreground.xml
Normal 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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
BIN
app-ui-catalog/src/main/res/mipmap-hdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
|
After Width: | Height: | Size: 2.7 KiB |
BIN
app-ui-catalog/src/main/res/mipmap-hdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 3.8 KiB |
BIN
app-ui-catalog/src/main/res/mipmap-mdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 1.7 KiB |
BIN
app-ui-catalog/src/main/res/mipmap-mdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 2.4 KiB |
BIN
app-ui-catalog/src/main/res/mipmap-xhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 2.5 KiB |
|
After Width: | Height: | Size: 3.8 KiB |
BIN
app-ui-catalog/src/main/res/mipmap-xhdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 5.4 KiB |
BIN
app-ui-catalog/src/main/res/mipmap-xxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 4 KiB |
|
After Width: | Height: | Size: 6 KiB |
BIN
app-ui-catalog/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 8.6 KiB |
BIN
app-ui-catalog/src/main/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 5.5 KiB |
|
After Width: | Height: | Size: 9.1 KiB |
BIN
app-ui-catalog/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
4
app-ui-catalog/src/main/res/values/colors.xml
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="ic_launcher_background">#FCE8DC</color>
|
||||
</resources>
|
||||
3
app-ui-catalog/src/main/res/values/strings.xml
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
<resources>
|
||||
<string name="app_name">Thunderbird Catalog</string>
|
||||
</resources>
|
||||
4
app-ui-catalog/src/main/res/values/themes.xml
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<style name="Theme.Thunderbird" parent="android:Theme.Material.Light.NoActionBar" />
|
||||
</resources>
|
||||
9
app/autodiscovery/api/build.gradle.kts
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
@Suppress("DSL_SCOPE_VIOLATION")
|
||||
plugins {
|
||||
id(ThunderbirdPlugins.Library.jvm)
|
||||
alias(libs.plugins.android.lint)
|
||||
}
|
||||
|
||||
dependencies {
|
||||
api(projects.mail.common)
|
||||
}
|
||||
|
|
@ -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?
|
||||
)
|
||||
20
app/autodiscovery/providersxml/build.gradle.kts
Normal 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"
|
||||
}
|
||||
|
|
@ -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()) }
|
||||
}
|
||||
|
|
@ -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
|
||||
)
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
774
app/autodiscovery/providersxml/src/main/res/xml/providers.xml
Normal 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>
|
||||
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
11
app/autodiscovery/srvrecords/build.gradle.kts
Normal 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)
|
||||
}
|
||||
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
package com.fsck.k9.autodiscovery.srvrecords
|
||||
|
||||
interface SrvResolver {
|
||||
fun lookup(domain: String, type: SrvType): List<MailService>
|
||||
}
|
||||
|
|
@ -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
|
||||
)
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
15
app/autodiscovery/thunderbird/build.gradle.kts
Normal 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)
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
|
@ -0,0 +1,51 @@
|
|||
@Suppress("DSL_SCOPE_VIOLATION")
|
||||
plugins {
|
||||
id(ThunderbirdPlugins.Library.android)
|
||||
alias(libs.plugins.kotlin.parcelize)
|
||||
}
|
||||
|
||||
dependencies {
|
||||
api(projects.mail.common)
|
||||
api(projects.backend.api)
|
||||
api(projects.app.htmlCleaner)
|
||||
api(projects.core.android.common)
|
||||
|
||||
implementation(projects.plugins.openpgpApiLib.openpgpApi)
|
||||
|
||||
api(libs.koin.android)
|
||||
|
||||
api(libs.androidx.annotation)
|
||||
|
||||
implementation(libs.okio)
|
||||
implementation(libs.commons.io)
|
||||
implementation(libs.androidx.core.ktx)
|
||||
implementation(libs.androidx.work.ktx)
|
||||
implementation(libs.androidx.fragment)
|
||||
implementation(libs.androidx.localbroadcastmanager)
|
||||
implementation(libs.jsoup)
|
||||
implementation(libs.moshi)
|
||||
implementation(libs.timber)
|
||||
implementation(libs.mime4j.core)
|
||||
implementation(libs.mime4j.dom)
|
||||
|
||||
testApi(projects.core.testing)
|
||||
testImplementation(projects.mail.testing)
|
||||
testImplementation(projects.backend.imap)
|
||||
testImplementation(projects.mail.protocols.smtp)
|
||||
testImplementation(projects.app.storage)
|
||||
testImplementation(projects.app.testing)
|
||||
|
||||
testImplementation(libs.kotlin.test)
|
||||
testImplementation(libs.kotlin.reflect)
|
||||
testImplementation(libs.robolectric)
|
||||
testImplementation(libs.androidx.test.core)
|
||||
testImplementation(libs.jdom2)
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.fsck.k9.core"
|
||||
|
||||
buildFeatures {
|
||||
buildConfig = true
|
||||
}
|
||||
}
|
||||
7
app/core/src/main/AndroidManifest.xml
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
|
||||
<uses-permission android:name="android.permission.READ_SYNC_SETTINGS" />
|
||||
|
||||
</manifest>
|
||||
696
app/core/src/main/java/com/fsck/k9/Account.kt
Normal file
|
|
@ -0,0 +1,696 @@
|
|||
package com.fsck.k9
|
||||
|
||||
import com.fsck.k9.backend.api.SyncConfig.ExpungePolicy
|
||||
import com.fsck.k9.mail.Address
|
||||
import com.fsck.k9.mail.ServerSettings
|
||||
import java.util.Calendar
|
||||
import java.util.Date
|
||||
|
||||
/**
|
||||
* Account stores all of the settings for a single account defined by the user. Each account is defined by a UUID.
|
||||
*/
|
||||
class Account(override val uuid: String) : BaseAccount {
|
||||
@get:Synchronized
|
||||
@set:Synchronized
|
||||
var deletePolicy = DeletePolicy.NEVER
|
||||
|
||||
@get:Synchronized
|
||||
@set:Synchronized
|
||||
private var internalIncomingServerSettings: ServerSettings? = null
|
||||
|
||||
@get:Synchronized
|
||||
@set:Synchronized
|
||||
private var internalOutgoingServerSettings: ServerSettings? = null
|
||||
|
||||
var incomingServerSettings: ServerSettings
|
||||
get() = internalIncomingServerSettings ?: error("Incoming server settings not set yet")
|
||||
set(value) {
|
||||
internalIncomingServerSettings = value
|
||||
}
|
||||
|
||||
var outgoingServerSettings: ServerSettings
|
||||
get() = internalOutgoingServerSettings ?: error("Outgoing server settings not set yet")
|
||||
set(value) {
|
||||
internalOutgoingServerSettings = value
|
||||
}
|
||||
|
||||
@get:Synchronized
|
||||
@set:Synchronized
|
||||
var oAuthState: String? = null
|
||||
|
||||
/**
|
||||
* Storage provider ID, used to locate and manage the underlying DB/file storage.
|
||||
*/
|
||||
@get:Synchronized
|
||||
@set:Synchronized
|
||||
var localStorageProviderId: String? = null
|
||||
|
||||
@get:Synchronized
|
||||
@set:Synchronized
|
||||
override var name: String? = null
|
||||
set(value) {
|
||||
field = value?.takeIf { it.isNotEmpty() }
|
||||
}
|
||||
|
||||
@get:Synchronized
|
||||
@set:Synchronized
|
||||
var alwaysBcc: String? = null
|
||||
|
||||
/**
|
||||
* -1 for never.
|
||||
*/
|
||||
@get:Synchronized
|
||||
@set:Synchronized
|
||||
var automaticCheckIntervalMinutes = 0
|
||||
|
||||
@get:Synchronized
|
||||
@set:Synchronized
|
||||
var displayCount = 0
|
||||
set(value) {
|
||||
if (field != value) {
|
||||
field = value.takeIf { it != -1 } ?: K9.DEFAULT_VISIBLE_LIMIT
|
||||
isChangedVisibleLimits = true
|
||||
}
|
||||
}
|
||||
|
||||
@get:Synchronized
|
||||
@set:Synchronized
|
||||
var chipColor = 0
|
||||
|
||||
@get:Synchronized
|
||||
@set:Synchronized
|
||||
var isNotifyNewMail = false
|
||||
|
||||
@get:Synchronized
|
||||
@set:Synchronized
|
||||
var folderNotifyNewMailMode = FolderMode.ALL
|
||||
|
||||
@get:Synchronized
|
||||
@set:Synchronized
|
||||
var isNotifySelfNewMail = false
|
||||
|
||||
@get:Synchronized
|
||||
@set:Synchronized
|
||||
var isNotifyContactsMailOnly = false
|
||||
|
||||
@get:Synchronized
|
||||
@set:Synchronized
|
||||
var isIgnoreChatMessages = false
|
||||
|
||||
@get:Synchronized
|
||||
@set:Synchronized
|
||||
var legacyInboxFolder: String? = null
|
||||
|
||||
@get:Synchronized
|
||||
@set:Synchronized
|
||||
var importedDraftsFolder: String? = null
|
||||
|
||||
@get:Synchronized
|
||||
@set:Synchronized
|
||||
var importedSentFolder: String? = null
|
||||
|
||||
@get:Synchronized
|
||||
@set:Synchronized
|
||||
var importedTrashFolder: String? = null
|
||||
|
||||
@get:Synchronized
|
||||
@set:Synchronized
|
||||
var importedArchiveFolder: String? = null
|
||||
|
||||
@get:Synchronized
|
||||
@set:Synchronized
|
||||
var importedSpamFolder: String? = null
|
||||
|
||||
@get:Synchronized
|
||||
@set:Synchronized
|
||||
var inboxFolderId: Long? = null
|
||||
|
||||
@get:Synchronized
|
||||
@set:Synchronized
|
||||
var outboxFolderId: Long? = null
|
||||
|
||||
@get:Synchronized
|
||||
@set:Synchronized
|
||||
var draftsFolderId: Long? = null
|
||||
|
||||
@get:Synchronized
|
||||
@set:Synchronized
|
||||
var sentFolderId: Long? = null
|
||||
|
||||
@get:Synchronized
|
||||
@set:Synchronized
|
||||
var trashFolderId: Long? = null
|
||||
|
||||
@get:Synchronized
|
||||
@set:Synchronized
|
||||
var archiveFolderId: Long? = null
|
||||
|
||||
@get:Synchronized
|
||||
@set:Synchronized
|
||||
var spamFolderId: Long? = null
|
||||
|
||||
@get:Synchronized
|
||||
var draftsFolderSelection = SpecialFolderSelection.AUTOMATIC
|
||||
private set
|
||||
|
||||
@get:Synchronized
|
||||
var sentFolderSelection = SpecialFolderSelection.AUTOMATIC
|
||||
private set
|
||||
|
||||
@get:Synchronized
|
||||
var trashFolderSelection = SpecialFolderSelection.AUTOMATIC
|
||||
private set
|
||||
|
||||
@get:Synchronized
|
||||
var archiveFolderSelection = SpecialFolderSelection.AUTOMATIC
|
||||
private set
|
||||
|
||||
@get:Synchronized
|
||||
var spamFolderSelection = SpecialFolderSelection.AUTOMATIC
|
||||
private set
|
||||
|
||||
@get:Synchronized
|
||||
@set:Synchronized
|
||||
var importedAutoExpandFolder: String? = null
|
||||
|
||||
@get:Synchronized
|
||||
@set:Synchronized
|
||||
var autoExpandFolderId: Long? = null
|
||||
|
||||
@get:Synchronized
|
||||
@set:Synchronized
|
||||
var folderDisplayMode = FolderMode.NOT_SECOND_CLASS
|
||||
|
||||
@get:Synchronized
|
||||
@set:Synchronized
|
||||
var folderSyncMode = FolderMode.FIRST_CLASS
|
||||
|
||||
@get:Synchronized
|
||||
@set:Synchronized
|
||||
var folderPushMode = FolderMode.NONE
|
||||
|
||||
@get:Synchronized
|
||||
@set:Synchronized
|
||||
var folderTargetMode = FolderMode.NOT_SECOND_CLASS
|
||||
|
||||
@get:Synchronized
|
||||
@set:Synchronized
|
||||
var accountNumber = 0
|
||||
|
||||
@get:Synchronized
|
||||
@set:Synchronized
|
||||
var isNotifySync = false
|
||||
|
||||
@get:Synchronized
|
||||
@set:Synchronized
|
||||
var sortType: SortType = SortType.SORT_DATE
|
||||
|
||||
private val sortAscending: MutableMap<SortType, Boolean> = mutableMapOf()
|
||||
|
||||
@get:Synchronized
|
||||
@set:Synchronized
|
||||
var showPictures = ShowPictures.NEVER
|
||||
|
||||
@get:Synchronized
|
||||
@set:Synchronized
|
||||
var isSignatureBeforeQuotedText = false
|
||||
|
||||
@get:Synchronized
|
||||
@set:Synchronized
|
||||
var expungePolicy = Expunge.EXPUNGE_IMMEDIATELY
|
||||
|
||||
@get:Synchronized
|
||||
@set:Synchronized
|
||||
var maxPushFolders = 0
|
||||
|
||||
@get:Synchronized
|
||||
@set:Synchronized
|
||||
var idleRefreshMinutes = 0
|
||||
|
||||
@get:JvmName("useCompression")
|
||||
@get:Synchronized
|
||||
@set:Synchronized
|
||||
var useCompression = true
|
||||
|
||||
@get:Synchronized
|
||||
@set:Synchronized
|
||||
var searchableFolders = Searchable.ALL
|
||||
|
||||
@get:Synchronized
|
||||
@set:Synchronized
|
||||
var isSubscribedFoldersOnly = false
|
||||
|
||||
@get:Synchronized
|
||||
@set:Synchronized
|
||||
var maximumPolledMessageAge = 0
|
||||
|
||||
@get:Synchronized
|
||||
@set:Synchronized
|
||||
var maximumAutoDownloadMessageSize = 0
|
||||
|
||||
@get:Synchronized
|
||||
@set:Synchronized
|
||||
var messageFormat = MessageFormat.HTML
|
||||
|
||||
@get:Synchronized
|
||||
@set:Synchronized
|
||||
var isMessageFormatAuto = false
|
||||
|
||||
@get:Synchronized
|
||||
@set:Synchronized
|
||||
var isMessageReadReceipt = false
|
||||
|
||||
@get:Synchronized
|
||||
@set:Synchronized
|
||||
var quoteStyle = QuoteStyle.PREFIX
|
||||
|
||||
@get:Synchronized
|
||||
@set:Synchronized
|
||||
var quotePrefix: String? = null
|
||||
|
||||
@get:Synchronized
|
||||
@set:Synchronized
|
||||
var isDefaultQuotedTextShown = false
|
||||
|
||||
@get:Synchronized
|
||||
@set:Synchronized
|
||||
var isReplyAfterQuote = false
|
||||
|
||||
@get:Synchronized
|
||||
@set:Synchronized
|
||||
var isStripSignature = false
|
||||
|
||||
@get:Synchronized
|
||||
@set:Synchronized
|
||||
var isSyncRemoteDeletions = false
|
||||
|
||||
@get:Synchronized
|
||||
@set:Synchronized
|
||||
var openPgpProvider: String? = null
|
||||
set(value) {
|
||||
field = value?.takeIf { it.isNotEmpty() }
|
||||
}
|
||||
|
||||
@get:Synchronized
|
||||
@set:Synchronized
|
||||
var openPgpKey: Long = 0
|
||||
|
||||
@get:Synchronized
|
||||
@set:Synchronized
|
||||
var autocryptPreferEncryptMutual = false
|
||||
|
||||
@get:Synchronized
|
||||
@set:Synchronized
|
||||
var isOpenPgpHideSignOnly = false
|
||||
|
||||
@get:Synchronized
|
||||
@set:Synchronized
|
||||
var isOpenPgpEncryptSubject = false
|
||||
|
||||
@get:Synchronized
|
||||
@set:Synchronized
|
||||
var isOpenPgpEncryptAllDrafts = false
|
||||
|
||||
@get:Synchronized
|
||||
@set:Synchronized
|
||||
var isMarkMessageAsReadOnView = false
|
||||
|
||||
@get:Synchronized
|
||||
@set:Synchronized
|
||||
var isMarkMessageAsReadOnDelete = false
|
||||
|
||||
@get:Synchronized
|
||||
@set:Synchronized
|
||||
var isAlwaysShowCcBcc = false
|
||||
|
||||
// Temporarily disabled
|
||||
@get:Synchronized
|
||||
@set:Synchronized
|
||||
var isRemoteSearchFullText = false
|
||||
get() = false
|
||||
|
||||
@get:Synchronized
|
||||
@set:Synchronized
|
||||
var remoteSearchNumResults = 0
|
||||
set(value) {
|
||||
field = value.coerceAtLeast(0)
|
||||
}
|
||||
|
||||
@get:Synchronized
|
||||
@set:Synchronized
|
||||
var isUploadSentMessages = false
|
||||
|
||||
@get:Synchronized
|
||||
@set:Synchronized
|
||||
var lastSyncTime: Long = 0
|
||||
|
||||
@get:Synchronized
|
||||
@set:Synchronized
|
||||
var lastFolderListRefreshTime: Long = 0
|
||||
|
||||
@get:Synchronized
|
||||
var isFinishedSetup = false
|
||||
private set
|
||||
|
||||
@get:Synchronized
|
||||
@set:Synchronized
|
||||
var messagesNotificationChannelVersion = 0
|
||||
|
||||
@get:Synchronized
|
||||
@set:Synchronized
|
||||
var isChangedVisibleLimits = false
|
||||
private set
|
||||
|
||||
/**
|
||||
* Database ID of the folder that was last selected for a copy or move operation.
|
||||
*
|
||||
* Note: For now this value isn't persisted. So it will be reset when K-9 Mail is restarted.
|
||||
*/
|
||||
@get:Synchronized
|
||||
var lastSelectedFolderId: Long? = null
|
||||
private set
|
||||
|
||||
@get:Synchronized
|
||||
@set:Synchronized
|
||||
var identities: MutableList<Identity> = mutableListOf()
|
||||
set(value) {
|
||||
field = value.toMutableList()
|
||||
}
|
||||
|
||||
@get:Synchronized
|
||||
var notificationSettings = NotificationSettings()
|
||||
private set
|
||||
|
||||
val displayName: String
|
||||
get() = name ?: email
|
||||
|
||||
@get:Synchronized
|
||||
@set:Synchronized
|
||||
override var email: String
|
||||
get() = identities[0].email!!
|
||||
set(email) {
|
||||
val newIdentity = identities[0].withEmail(email)
|
||||
identities[0] = newIdentity
|
||||
}
|
||||
|
||||
@get:Synchronized
|
||||
@set:Synchronized
|
||||
var senderName: String?
|
||||
get() = identities[0].name
|
||||
set(name) {
|
||||
val newIdentity = identities[0].withName(name)
|
||||
identities[0] = newIdentity
|
||||
}
|
||||
|
||||
@get:Synchronized
|
||||
@set:Synchronized
|
||||
var signatureUse: Boolean
|
||||
get() = identities[0].signatureUse
|
||||
set(signatureUse) {
|
||||
val newIdentity = identities[0].withSignatureUse(signatureUse)
|
||||
identities[0] = newIdentity
|
||||
}
|
||||
|
||||
@get:Synchronized
|
||||
@set:Synchronized
|
||||
var signature: String?
|
||||
get() = identities[0].signature
|
||||
set(signature) {
|
||||
val newIdentity = identities[0].withSignature(signature)
|
||||
identities[0] = newIdentity
|
||||
}
|
||||
|
||||
@get:JvmName("shouldMigrateToOAuth")
|
||||
@get:Synchronized
|
||||
@set:Synchronized
|
||||
var shouldMigrateToOAuth = false
|
||||
|
||||
/**
|
||||
* @param automaticCheckIntervalMinutes or -1 for never.
|
||||
*/
|
||||
@Synchronized
|
||||
fun updateAutomaticCheckIntervalMinutes(automaticCheckIntervalMinutes: Int): Boolean {
|
||||
val oldInterval = this.automaticCheckIntervalMinutes
|
||||
this.automaticCheckIntervalMinutes = automaticCheckIntervalMinutes
|
||||
|
||||
return oldInterval != automaticCheckIntervalMinutes
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun setDraftsFolderId(folderId: Long?, selection: SpecialFolderSelection) {
|
||||
draftsFolderId = folderId
|
||||
draftsFolderSelection = selection
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun hasDraftsFolder(): Boolean {
|
||||
return draftsFolderId != null
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun setSentFolderId(folderId: Long?, selection: SpecialFolderSelection) {
|
||||
sentFolderId = folderId
|
||||
sentFolderSelection = selection
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun hasSentFolder(): Boolean {
|
||||
return sentFolderId != null
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun setTrashFolderId(folderId: Long?, selection: SpecialFolderSelection) {
|
||||
trashFolderId = folderId
|
||||
trashFolderSelection = selection
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun hasTrashFolder(): Boolean {
|
||||
return trashFolderId != null
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun setArchiveFolderId(folderId: Long?, selection: SpecialFolderSelection) {
|
||||
archiveFolderId = folderId
|
||||
archiveFolderSelection = selection
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun hasArchiveFolder(): Boolean {
|
||||
return archiveFolderId != null
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun setSpamFolderId(folderId: Long?, selection: SpecialFolderSelection) {
|
||||
spamFolderId = folderId
|
||||
spamFolderSelection = selection
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun hasSpamFolder(): Boolean {
|
||||
return spamFolderId != null
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun updateFolderSyncMode(syncMode: FolderMode): Boolean {
|
||||
val oldSyncMode = folderSyncMode
|
||||
folderSyncMode = syncMode
|
||||
|
||||
return (oldSyncMode == FolderMode.NONE && syncMode != FolderMode.NONE) ||
|
||||
(oldSyncMode != FolderMode.NONE && syncMode == FolderMode.NONE)
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun isSortAscending(sortType: SortType): Boolean {
|
||||
return sortAscending.getOrPut(sortType) { sortType.isDefaultAscending }
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun setSortAscending(sortType: SortType, sortAscending: Boolean) {
|
||||
this.sortAscending[sortType] = sortAscending
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun replaceIdentities(identities: List<Identity>) {
|
||||
this.identities = identities.toMutableList()
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun getIdentity(index: Int): Identity {
|
||||
if (index !in identities.indices) error("Identity with index $index not found")
|
||||
|
||||
return identities[index]
|
||||
}
|
||||
|
||||
fun isAnIdentity(addresses: Array<Address>?): Boolean {
|
||||
if (addresses == null) return false
|
||||
|
||||
return addresses.any { address -> isAnIdentity(address) }
|
||||
}
|
||||
|
||||
fun isAnIdentity(address: Address): Boolean {
|
||||
return findIdentity(address) != null
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun findIdentity(address: Address): Identity? {
|
||||
return identities.find { identity ->
|
||||
identity.email.equals(address.address, ignoreCase = true)
|
||||
}
|
||||
}
|
||||
|
||||
val earliestPollDate: Date?
|
||||
get() {
|
||||
val age = maximumPolledMessageAge.takeIf { it >= 0 } ?: return null
|
||||
|
||||
val now = Calendar.getInstance()
|
||||
now[Calendar.HOUR_OF_DAY] = 0
|
||||
now[Calendar.MINUTE] = 0
|
||||
now[Calendar.SECOND] = 0
|
||||
now[Calendar.MILLISECOND] = 0
|
||||
|
||||
if (age < 28) {
|
||||
now.add(Calendar.DATE, age * -1)
|
||||
} else {
|
||||
when (age) {
|
||||
28 -> now.add(Calendar.MONTH, -1)
|
||||
56 -> now.add(Calendar.MONTH, -2)
|
||||
84 -> now.add(Calendar.MONTH, -3)
|
||||
168 -> now.add(Calendar.MONTH, -6)
|
||||
365 -> now.add(Calendar.YEAR, -1)
|
||||
}
|
||||
}
|
||||
|
||||
return now.time
|
||||
}
|
||||
|
||||
val isOpenPgpProviderConfigured: Boolean
|
||||
get() = openPgpProvider != null
|
||||
|
||||
@Synchronized
|
||||
fun hasOpenPgpKey(): Boolean {
|
||||
return openPgpKey != NO_OPENPGP_KEY
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun setLastSelectedFolderId(folderId: Long) {
|
||||
lastSelectedFolderId = folderId
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun resetChangeMarkers() {
|
||||
isChangedVisibleLimits = false
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun markSetupFinished() {
|
||||
isFinishedSetup = true
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun updateNotificationSettings(block: (oldNotificationSettings: NotificationSettings) -> NotificationSettings) {
|
||||
notificationSettings = block(notificationSettings)
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return if (K9.isSensitiveDebugLoggingEnabled) displayName else uuid
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
return if (other is Account) {
|
||||
other.uuid == uuid
|
||||
} else {
|
||||
super.equals(other)
|
||||
}
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
return uuid.hashCode()
|
||||
}
|
||||
|
||||
enum class FolderMode {
|
||||
NONE,
|
||||
ALL,
|
||||
FIRST_CLASS,
|
||||
FIRST_AND_SECOND_CLASS,
|
||||
NOT_SECOND_CLASS
|
||||
}
|
||||
|
||||
enum class SpecialFolderSelection {
|
||||
AUTOMATIC,
|
||||
MANUAL
|
||||
}
|
||||
|
||||
enum class ShowPictures {
|
||||
NEVER,
|
||||
ALWAYS,
|
||||
ONLY_FROM_CONTACTS
|
||||
}
|
||||
|
||||
enum class Searchable {
|
||||
ALL,
|
||||
DISPLAYABLE,
|
||||
NONE
|
||||
}
|
||||
|
||||
enum class QuoteStyle {
|
||||
PREFIX,
|
||||
HEADER
|
||||
}
|
||||
|
||||
enum class MessageFormat {
|
||||
TEXT,
|
||||
HTML,
|
||||
AUTO
|
||||
}
|
||||
|
||||
enum class Expunge {
|
||||
EXPUNGE_IMMEDIATELY,
|
||||
EXPUNGE_MANUALLY,
|
||||
EXPUNGE_ON_POLL;
|
||||
|
||||
fun toBackendExpungePolicy(): ExpungePolicy = when (this) {
|
||||
EXPUNGE_IMMEDIATELY -> ExpungePolicy.IMMEDIATELY
|
||||
EXPUNGE_MANUALLY -> ExpungePolicy.MANUALLY
|
||||
EXPUNGE_ON_POLL -> ExpungePolicy.ON_POLL
|
||||
}
|
||||
}
|
||||
|
||||
enum class DeletePolicy(@JvmField val setting: Int) {
|
||||
NEVER(0),
|
||||
SEVEN_DAYS(1),
|
||||
ON_DELETE(2),
|
||||
MARK_AS_READ(3);
|
||||
|
||||
companion object {
|
||||
fun fromInt(initialSetting: Int): DeletePolicy {
|
||||
return values().find { it.setting == initialSetting } ?: error("DeletePolicy $initialSetting unknown")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum class SortType(val isDefaultAscending: Boolean) {
|
||||
SORT_DATE(false),
|
||||
SORT_ARRIVAL(false),
|
||||
SORT_SUBJECT(true),
|
||||
SORT_SENDER(true),
|
||||
SORT_UNREAD(true),
|
||||
SORT_FLAGGED(true),
|
||||
SORT_ATTACHMENT(true);
|
||||
}
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* Fixed name of outbox - not actually displayed.
|
||||
*/
|
||||
const val OUTBOX_NAME = "Outbox"
|
||||
|
||||
@JvmField
|
||||
val DEFAULT_SORT_TYPE = SortType.SORT_DATE
|
||||
const val DEFAULT_SORT_ASCENDING = false
|
||||
const val NO_OPENPGP_KEY: Long = 0
|
||||
const val UNASSIGNED_ACCOUNT_NUMBER = -1
|
||||
const val INTERVAL_MINUTES_NEVER = -1
|
||||
const val DEFAULT_SYNC_INTERVAL = 60
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,648 @@
|
|||
package com.fsck.k9
|
||||
|
||||
import com.fsck.k9.Account.Companion.DEFAULT_SORT_ASCENDING
|
||||
import com.fsck.k9.Account.Companion.DEFAULT_SORT_TYPE
|
||||
import com.fsck.k9.Account.Companion.DEFAULT_SYNC_INTERVAL
|
||||
import com.fsck.k9.Account.Companion.NO_OPENPGP_KEY
|
||||
import com.fsck.k9.Account.Companion.UNASSIGNED_ACCOUNT_NUMBER
|
||||
import com.fsck.k9.Account.DeletePolicy
|
||||
import com.fsck.k9.Account.Expunge
|
||||
import com.fsck.k9.Account.FolderMode
|
||||
import com.fsck.k9.Account.MessageFormat
|
||||
import com.fsck.k9.Account.QuoteStyle
|
||||
import com.fsck.k9.Account.Searchable
|
||||
import com.fsck.k9.Account.ShowPictures
|
||||
import com.fsck.k9.Account.SortType
|
||||
import com.fsck.k9.Account.SpecialFolderSelection
|
||||
import com.fsck.k9.helper.Utility
|
||||
import com.fsck.k9.mailstore.StorageManager
|
||||
import com.fsck.k9.preferences.Storage
|
||||
import com.fsck.k9.preferences.StorageEditor
|
||||
import timber.log.Timber
|
||||
|
||||
class AccountPreferenceSerializer(
|
||||
private val storageManager: StorageManager,
|
||||
private val resourceProvider: CoreResourceProvider,
|
||||
private val serverSettingsSerializer: ServerSettingsSerializer
|
||||
) {
|
||||
|
||||
@Synchronized
|
||||
fun loadAccount(account: Account, storage: Storage) {
|
||||
val accountUuid = account.uuid
|
||||
with(account) {
|
||||
incomingServerSettings = serverSettingsSerializer.deserialize(
|
||||
storage.getString("$accountUuid.$INCOMING_SERVER_SETTINGS_KEY", "")
|
||||
)
|
||||
outgoingServerSettings = serverSettingsSerializer.deserialize(
|
||||
storage.getString("$accountUuid.$OUTGOING_SERVER_SETTINGS_KEY", "")
|
||||
)
|
||||
oAuthState = storage.getString("$accountUuid.oAuthState", null)
|
||||
localStorageProviderId = storage.getString("$accountUuid.localStorageProvider", storageManager.defaultProviderId)
|
||||
name = storage.getString("$accountUuid.description", null)
|
||||
alwaysBcc = storage.getString("$accountUuid.alwaysBcc", alwaysBcc)
|
||||
automaticCheckIntervalMinutes = storage.getInt("$accountUuid.automaticCheckIntervalMinutes", DEFAULT_SYNC_INTERVAL)
|
||||
idleRefreshMinutes = storage.getInt("$accountUuid.idleRefreshMinutes", 24)
|
||||
displayCount = storage.getInt("$accountUuid.displayCount", K9.DEFAULT_VISIBLE_LIMIT)
|
||||
if (displayCount < 0) {
|
||||
displayCount = K9.DEFAULT_VISIBLE_LIMIT
|
||||
}
|
||||
isNotifyNewMail = storage.getBoolean("$accountUuid.notifyNewMail", false)
|
||||
|
||||
folderNotifyNewMailMode = getEnumStringPref<FolderMode>(storage, "$accountUuid.folderNotifyNewMailMode", FolderMode.ALL)
|
||||
isNotifySelfNewMail = storage.getBoolean("$accountUuid.notifySelfNewMail", true)
|
||||
isNotifyContactsMailOnly = storage.getBoolean("$accountUuid.notifyContactsMailOnly", false)
|
||||
isIgnoreChatMessages = storage.getBoolean("$accountUuid.ignoreChatMessages", false)
|
||||
isNotifySync = storage.getBoolean("$accountUuid.notifyMailCheck", false)
|
||||
messagesNotificationChannelVersion = storage.getInt("$accountUuid.messagesNotificationChannelVersion", 0)
|
||||
deletePolicy = DeletePolicy.fromInt(storage.getInt("$accountUuid.deletePolicy", DeletePolicy.NEVER.setting))
|
||||
legacyInboxFolder = storage.getString("$accountUuid.inboxFolderName", null)
|
||||
importedDraftsFolder = storage.getString("$accountUuid.draftsFolderName", null)
|
||||
importedSentFolder = storage.getString("$accountUuid.sentFolderName", null)
|
||||
importedTrashFolder = storage.getString("$accountUuid.trashFolderName", null)
|
||||
importedArchiveFolder = storage.getString("$accountUuid.archiveFolderName", null)
|
||||
importedSpamFolder = storage.getString("$accountUuid.spamFolderName", null)
|
||||
|
||||
inboxFolderId = storage.getString("$accountUuid.inboxFolderId", null)?.toLongOrNull()
|
||||
outboxFolderId = storage.getString("$accountUuid.outboxFolderId", null)?.toLongOrNull()
|
||||
|
||||
val draftsFolderId = storage.getString("$accountUuid.draftsFolderId", null)?.toLongOrNull()
|
||||
val draftsFolderSelection = getEnumStringPref<SpecialFolderSelection>(
|
||||
storage,
|
||||
"$accountUuid.draftsFolderSelection",
|
||||
SpecialFolderSelection.AUTOMATIC
|
||||
)
|
||||
setDraftsFolderId(draftsFolderId, draftsFolderSelection)
|
||||
|
||||
val sentFolderId = storage.getString("$accountUuid.sentFolderId", null)?.toLongOrNull()
|
||||
val sentFolderSelection = getEnumStringPref<SpecialFolderSelection>(
|
||||
storage,
|
||||
"$accountUuid.sentFolderSelection",
|
||||
SpecialFolderSelection.AUTOMATIC
|
||||
)
|
||||
setSentFolderId(sentFolderId, sentFolderSelection)
|
||||
|
||||
val trashFolderId = storage.getString("$accountUuid.trashFolderId", null)?.toLongOrNull()
|
||||
val trashFolderSelection = getEnumStringPref<SpecialFolderSelection>(
|
||||
storage,
|
||||
"$accountUuid.trashFolderSelection",
|
||||
SpecialFolderSelection.AUTOMATIC
|
||||
)
|
||||
setTrashFolderId(trashFolderId, trashFolderSelection)
|
||||
|
||||
val archiveFolderId = storage.getString("$accountUuid.archiveFolderId", null)?.toLongOrNull()
|
||||
val archiveFolderSelection = getEnumStringPref<SpecialFolderSelection>(
|
||||
storage,
|
||||
"$accountUuid.archiveFolderSelection",
|
||||
SpecialFolderSelection.AUTOMATIC
|
||||
)
|
||||
setArchiveFolderId(archiveFolderId, archiveFolderSelection)
|
||||
|
||||
val spamFolderId = storage.getString("$accountUuid.spamFolderId", null)?.toLongOrNull()
|
||||
val spamFolderSelection = getEnumStringPref<SpecialFolderSelection>(
|
||||
storage,
|
||||
"$accountUuid.spamFolderSelection",
|
||||
SpecialFolderSelection.AUTOMATIC
|
||||
)
|
||||
setSpamFolderId(spamFolderId, spamFolderSelection)
|
||||
|
||||
autoExpandFolderId = storage.getString("$accountUuid.autoExpandFolderId", null)?.toLongOrNull()
|
||||
|
||||
expungePolicy = getEnumStringPref<Expunge>(storage, "$accountUuid.expungePolicy", Expunge.EXPUNGE_IMMEDIATELY)
|
||||
isSyncRemoteDeletions = storage.getBoolean("$accountUuid.syncRemoteDeletions", true)
|
||||
|
||||
maxPushFolders = storage.getInt("$accountUuid.maxPushFolders", 10)
|
||||
isSubscribedFoldersOnly = storage.getBoolean("$accountUuid.subscribedFoldersOnly", false)
|
||||
maximumPolledMessageAge = storage.getInt("$accountUuid.maximumPolledMessageAge", -1)
|
||||
maximumAutoDownloadMessageSize = storage.getInt("$accountUuid.maximumAutoDownloadMessageSize", 32768)
|
||||
messageFormat = getEnumStringPref<MessageFormat>(storage, "$accountUuid.messageFormat", DEFAULT_MESSAGE_FORMAT)
|
||||
val messageFormatAuto = storage.getBoolean("$accountUuid.messageFormatAuto", DEFAULT_MESSAGE_FORMAT_AUTO)
|
||||
if (messageFormatAuto && messageFormat == MessageFormat.TEXT) {
|
||||
messageFormat = MessageFormat.AUTO
|
||||
}
|
||||
isMessageReadReceipt = storage.getBoolean("$accountUuid.messageReadReceipt", DEFAULT_MESSAGE_READ_RECEIPT)
|
||||
quoteStyle = getEnumStringPref<QuoteStyle>(storage, "$accountUuid.quoteStyle", DEFAULT_QUOTE_STYLE)
|
||||
quotePrefix = storage.getString("$accountUuid.quotePrefix", DEFAULT_QUOTE_PREFIX)
|
||||
isDefaultQuotedTextShown = storage.getBoolean("$accountUuid.defaultQuotedTextShown", DEFAULT_QUOTED_TEXT_SHOWN)
|
||||
isReplyAfterQuote = storage.getBoolean("$accountUuid.replyAfterQuote", DEFAULT_REPLY_AFTER_QUOTE)
|
||||
isStripSignature = storage.getBoolean("$accountUuid.stripSignature", DEFAULT_STRIP_SIGNATURE)
|
||||
useCompression = storage.getBoolean("$accountUuid.useCompression", true)
|
||||
|
||||
importedAutoExpandFolder = storage.getString("$accountUuid.autoExpandFolderName", null)
|
||||
|
||||
accountNumber = storage.getInt("$accountUuid.accountNumber", UNASSIGNED_ACCOUNT_NUMBER)
|
||||
|
||||
chipColor = storage.getInt("$accountUuid.chipColor", FALLBACK_ACCOUNT_COLOR)
|
||||
|
||||
sortType = getEnumStringPref<SortType>(storage, "$accountUuid.sortTypeEnum", SortType.SORT_DATE)
|
||||
|
||||
setSortAscending(sortType, storage.getBoolean("$accountUuid.sortAscending", false))
|
||||
|
||||
showPictures = getEnumStringPref<ShowPictures>(storage, "$accountUuid.showPicturesEnum", ShowPictures.NEVER)
|
||||
|
||||
updateNotificationSettings {
|
||||
NotificationSettings(
|
||||
isRingEnabled = storage.getBoolean("$accountUuid.ring", true),
|
||||
ringtone = storage.getString("$accountUuid.ringtone", DEFAULT_RINGTONE_URI),
|
||||
light = getEnumStringPref(storage, "$accountUuid.notificationLight", NotificationLight.Disabled),
|
||||
vibration = NotificationVibration(
|
||||
isEnabled = storage.getBoolean("$accountUuid.vibrate", false),
|
||||
pattern = VibratePattern.deserialize(storage.getInt("$accountUuid.vibratePattern", 0)),
|
||||
repeatCount = storage.getInt("$accountUuid.vibrateTimes", 5)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
folderDisplayMode = getEnumStringPref<FolderMode>(storage, "$accountUuid.folderDisplayMode", FolderMode.NOT_SECOND_CLASS)
|
||||
|
||||
folderSyncMode = getEnumStringPref<FolderMode>(storage, "$accountUuid.folderSyncMode", FolderMode.FIRST_CLASS)
|
||||
|
||||
folderPushMode = getEnumStringPref<FolderMode>(storage, "$accountUuid.folderPushMode", FolderMode.NONE)
|
||||
|
||||
folderTargetMode = getEnumStringPref<FolderMode>(storage, "$accountUuid.folderTargetMode", FolderMode.NOT_SECOND_CLASS)
|
||||
|
||||
searchableFolders = getEnumStringPref<Searchable>(storage, "$accountUuid.searchableFolders", Searchable.ALL)
|
||||
|
||||
isSignatureBeforeQuotedText = storage.getBoolean("$accountUuid.signatureBeforeQuotedText", false)
|
||||
replaceIdentities(loadIdentities(accountUuid, storage))
|
||||
|
||||
openPgpProvider = storage.getString("$accountUuid.openPgpProvider", "")
|
||||
openPgpKey = storage.getLong("$accountUuid.cryptoKey", NO_OPENPGP_KEY)
|
||||
isOpenPgpHideSignOnly = storage.getBoolean("$accountUuid.openPgpHideSignOnly", true)
|
||||
isOpenPgpEncryptSubject = storage.getBoolean("$accountUuid.openPgpEncryptSubject", true)
|
||||
isOpenPgpEncryptAllDrafts = storage.getBoolean("$accountUuid.openPgpEncryptAllDrafts", true)
|
||||
autocryptPreferEncryptMutual = storage.getBoolean("$accountUuid.autocryptMutualMode", false)
|
||||
isRemoteSearchFullText = storage.getBoolean("$accountUuid.remoteSearchFullText", false)
|
||||
remoteSearchNumResults = storage.getInt("$accountUuid.remoteSearchNumResults", DEFAULT_REMOTE_SEARCH_NUM_RESULTS)
|
||||
isUploadSentMessages = storage.getBoolean("$accountUuid.uploadSentMessages", true)
|
||||
|
||||
isMarkMessageAsReadOnView = storage.getBoolean("$accountUuid.markMessageAsReadOnView", true)
|
||||
isMarkMessageAsReadOnDelete = storage.getBoolean("$accountUuid.markMessageAsReadOnDelete", true)
|
||||
isAlwaysShowCcBcc = storage.getBoolean("$accountUuid.alwaysShowCcBcc", false)
|
||||
lastSyncTime = storage.getLong("$accountUuid.lastSyncTime", 0L)
|
||||
lastFolderListRefreshTime = storage.getLong("$accountUuid.lastFolderListRefreshTime", 0L)
|
||||
|
||||
shouldMigrateToOAuth = storage.getBoolean("$accountUuid.migrateToOAuth", false)
|
||||
|
||||
val isFinishedSetup = storage.getBoolean("$accountUuid.isFinishedSetup", true)
|
||||
if (isFinishedSetup) markSetupFinished()
|
||||
|
||||
resetChangeMarkers()
|
||||
}
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
private fun loadIdentities(accountUuid: String, storage: Storage): List<Identity> {
|
||||
val newIdentities = ArrayList<Identity>()
|
||||
var ident = 0
|
||||
var gotOne: Boolean
|
||||
do {
|
||||
gotOne = false
|
||||
val name = storage.getString("$accountUuid.$IDENTITY_NAME_KEY.$ident", null)
|
||||
val email = storage.getString("$accountUuid.$IDENTITY_EMAIL_KEY.$ident", null)
|
||||
val signatureUse = storage.getBoolean("$accountUuid.signatureUse.$ident", false)
|
||||
val signature = storage.getString("$accountUuid.signature.$ident", null)
|
||||
val description = storage.getString("$accountUuid.$IDENTITY_DESCRIPTION_KEY.$ident", null)
|
||||
val replyTo = storage.getString("$accountUuid.replyTo.$ident", null)
|
||||
if (email != null) {
|
||||
val identity = Identity(
|
||||
name = name,
|
||||
email = email,
|
||||
signatureUse = signatureUse,
|
||||
signature = signature,
|
||||
description = description,
|
||||
replyTo = replyTo
|
||||
)
|
||||
newIdentities.add(identity)
|
||||
gotOne = true
|
||||
}
|
||||
ident++
|
||||
} while (gotOne)
|
||||
|
||||
if (newIdentities.isEmpty()) {
|
||||
val name = storage.getString("$accountUuid.name", null)
|
||||
val email = storage.getString("$accountUuid.email", null)
|
||||
val signatureUse = storage.getBoolean("$accountUuid.signatureUse", false)
|
||||
val signature = storage.getString("$accountUuid.signature", null)
|
||||
val identity = Identity(
|
||||
name = name,
|
||||
email = email,
|
||||
signatureUse = signatureUse,
|
||||
signature = signature,
|
||||
description = email
|
||||
)
|
||||
newIdentities.add(identity)
|
||||
}
|
||||
|
||||
return newIdentities
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun save(editor: StorageEditor, storage: Storage, account: Account) {
|
||||
val accountUuid = account.uuid
|
||||
|
||||
if (!storage.getString("accountUuids", "").contains(account.uuid)) {
|
||||
var accountUuids = storage.getString("accountUuids", "")
|
||||
accountUuids += (if (accountUuids.isNotEmpty()) "," else "") + account.uuid
|
||||
editor.putString("accountUuids", accountUuids)
|
||||
}
|
||||
|
||||
with(account) {
|
||||
editor.putString("$accountUuid.$INCOMING_SERVER_SETTINGS_KEY", serverSettingsSerializer.serialize(incomingServerSettings))
|
||||
editor.putString("$accountUuid.$OUTGOING_SERVER_SETTINGS_KEY", serverSettingsSerializer.serialize(outgoingServerSettings))
|
||||
editor.putString("$accountUuid.oAuthState", oAuthState)
|
||||
editor.putString("$accountUuid.localStorageProvider", localStorageProviderId)
|
||||
editor.putString("$accountUuid.description", name)
|
||||
editor.putString("$accountUuid.alwaysBcc", alwaysBcc)
|
||||
editor.putInt("$accountUuid.automaticCheckIntervalMinutes", automaticCheckIntervalMinutes)
|
||||
editor.putInt("$accountUuid.idleRefreshMinutes", idleRefreshMinutes)
|
||||
editor.putInt("$accountUuid.displayCount", displayCount)
|
||||
editor.putBoolean("$accountUuid.notifyNewMail", isNotifyNewMail)
|
||||
editor.putString("$accountUuid.folderNotifyNewMailMode", folderNotifyNewMailMode.name)
|
||||
editor.putBoolean("$accountUuid.notifySelfNewMail", isNotifySelfNewMail)
|
||||
editor.putBoolean("$accountUuid.notifyContactsMailOnly", isNotifyContactsMailOnly)
|
||||
editor.putBoolean("$accountUuid.ignoreChatMessages", isIgnoreChatMessages)
|
||||
editor.putBoolean("$accountUuid.notifyMailCheck", isNotifySync)
|
||||
editor.putInt("$accountUuid.messagesNotificationChannelVersion", messagesNotificationChannelVersion)
|
||||
editor.putInt("$accountUuid.deletePolicy", deletePolicy.setting)
|
||||
editor.putString("$accountUuid.inboxFolderName", legacyInboxFolder)
|
||||
editor.putString("$accountUuid.draftsFolderName", importedDraftsFolder)
|
||||
editor.putString("$accountUuid.sentFolderName", importedSentFolder)
|
||||
editor.putString("$accountUuid.trashFolderName", importedTrashFolder)
|
||||
editor.putString("$accountUuid.archiveFolderName", importedArchiveFolder)
|
||||
editor.putString("$accountUuid.spamFolderName", importedSpamFolder)
|
||||
editor.putString("$accountUuid.inboxFolderId", inboxFolderId?.toString())
|
||||
editor.putString("$accountUuid.outboxFolderId", outboxFolderId?.toString())
|
||||
editor.putString("$accountUuid.draftsFolderId", draftsFolderId?.toString())
|
||||
editor.putString("$accountUuid.sentFolderId", sentFolderId?.toString())
|
||||
editor.putString("$accountUuid.trashFolderId", trashFolderId?.toString())
|
||||
editor.putString("$accountUuid.archiveFolderId", archiveFolderId?.toString())
|
||||
editor.putString("$accountUuid.spamFolderId", spamFolderId?.toString())
|
||||
editor.putString("$accountUuid.archiveFolderSelection", archiveFolderSelection.name)
|
||||
editor.putString("$accountUuid.draftsFolderSelection", draftsFolderSelection.name)
|
||||
editor.putString("$accountUuid.sentFolderSelection", sentFolderSelection.name)
|
||||
editor.putString("$accountUuid.spamFolderSelection", spamFolderSelection.name)
|
||||
editor.putString("$accountUuid.trashFolderSelection", trashFolderSelection.name)
|
||||
editor.putString("$accountUuid.autoExpandFolderName", importedAutoExpandFolder)
|
||||
editor.putString("$accountUuid.autoExpandFolderId", autoExpandFolderId?.toString())
|
||||
editor.putInt("$accountUuid.accountNumber", accountNumber)
|
||||
editor.putString("$accountUuid.sortTypeEnum", sortType.name)
|
||||
editor.putBoolean("$accountUuid.sortAscending", isSortAscending(sortType))
|
||||
editor.putString("$accountUuid.showPicturesEnum", showPictures.name)
|
||||
editor.putString("$accountUuid.folderDisplayMode", folderDisplayMode.name)
|
||||
editor.putString("$accountUuid.folderSyncMode", folderSyncMode.name)
|
||||
editor.putString("$accountUuid.folderPushMode", folderPushMode.name)
|
||||
editor.putString("$accountUuid.folderTargetMode", folderTargetMode.name)
|
||||
editor.putBoolean("$accountUuid.signatureBeforeQuotedText", isSignatureBeforeQuotedText)
|
||||
editor.putString("$accountUuid.expungePolicy", expungePolicy.name)
|
||||
editor.putBoolean("$accountUuid.syncRemoteDeletions", isSyncRemoteDeletions)
|
||||
editor.putInt("$accountUuid.maxPushFolders", maxPushFolders)
|
||||
editor.putString("$accountUuid.searchableFolders", searchableFolders.name)
|
||||
editor.putInt("$accountUuid.chipColor", chipColor)
|
||||
editor.putBoolean("$accountUuid.subscribedFoldersOnly", isSubscribedFoldersOnly)
|
||||
editor.putInt("$accountUuid.maximumPolledMessageAge", maximumPolledMessageAge)
|
||||
editor.putInt("$accountUuid.maximumAutoDownloadMessageSize", maximumAutoDownloadMessageSize)
|
||||
val messageFormatAuto = if (MessageFormat.AUTO == messageFormat) {
|
||||
// saving MessageFormat.AUTO as is to the database will cause downgrades to crash on
|
||||
// startup, so we save as MessageFormat.TEXT instead with a separate flag for auto.
|
||||
editor.putString("$accountUuid.messageFormat", MessageFormat.TEXT.name)
|
||||
true
|
||||
} else {
|
||||
editor.putString("$accountUuid.messageFormat", messageFormat.name)
|
||||
false
|
||||
}
|
||||
editor.putBoolean("$accountUuid.messageFormatAuto", messageFormatAuto)
|
||||
editor.putBoolean("$accountUuid.messageReadReceipt", isMessageReadReceipt)
|
||||
editor.putString("$accountUuid.quoteStyle", quoteStyle.name)
|
||||
editor.putString("$accountUuid.quotePrefix", quotePrefix)
|
||||
editor.putBoolean("$accountUuid.defaultQuotedTextShown", isDefaultQuotedTextShown)
|
||||
editor.putBoolean("$accountUuid.replyAfterQuote", isReplyAfterQuote)
|
||||
editor.putBoolean("$accountUuid.stripSignature", isStripSignature)
|
||||
editor.putLong("$accountUuid.cryptoKey", openPgpKey)
|
||||
editor.putBoolean("$accountUuid.openPgpHideSignOnly", isOpenPgpHideSignOnly)
|
||||
editor.putBoolean("$accountUuid.openPgpEncryptSubject", isOpenPgpEncryptSubject)
|
||||
editor.putBoolean("$accountUuid.openPgpEncryptAllDrafts", isOpenPgpEncryptAllDrafts)
|
||||
editor.putString("$accountUuid.openPgpProvider", openPgpProvider)
|
||||
editor.putBoolean("$accountUuid.autocryptMutualMode", autocryptPreferEncryptMutual)
|
||||
editor.putBoolean("$accountUuid.remoteSearchFullText", isRemoteSearchFullText)
|
||||
editor.putInt("$accountUuid.remoteSearchNumResults", remoteSearchNumResults)
|
||||
editor.putBoolean("$accountUuid.uploadSentMessages", isUploadSentMessages)
|
||||
editor.putBoolean("$accountUuid.markMessageAsReadOnView", isMarkMessageAsReadOnView)
|
||||
editor.putBoolean("$accountUuid.markMessageAsReadOnDelete", isMarkMessageAsReadOnDelete)
|
||||
editor.putBoolean("$accountUuid.alwaysShowCcBcc", isAlwaysShowCcBcc)
|
||||
|
||||
editor.putBoolean("$accountUuid.vibrate", notificationSettings.vibration.isEnabled)
|
||||
editor.putInt("$accountUuid.vibratePattern", notificationSettings.vibration.pattern.serialize())
|
||||
editor.putInt("$accountUuid.vibrateTimes", notificationSettings.vibration.repeatCount)
|
||||
editor.putBoolean("$accountUuid.ring", notificationSettings.isRingEnabled)
|
||||
editor.putString("$accountUuid.ringtone", notificationSettings.ringtone)
|
||||
editor.putString("$accountUuid.notificationLight", notificationSettings.light.name)
|
||||
editor.putLong("$accountUuid.lastSyncTime", lastSyncTime)
|
||||
editor.putLong("$accountUuid.lastFolderListRefreshTime", lastFolderListRefreshTime)
|
||||
editor.putBoolean("$accountUuid.isFinishedSetup", isFinishedSetup)
|
||||
editor.putBoolean("$accountUuid.useCompression", useCompression)
|
||||
editor.putBoolean("$accountUuid.migrateToOAuth", shouldMigrateToOAuth)
|
||||
}
|
||||
|
||||
saveIdentities(account, storage, editor)
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun delete(editor: StorageEditor, storage: Storage, account: Account) {
|
||||
val accountUuid = account.uuid
|
||||
|
||||
// Get the list of account UUIDs
|
||||
val uuids = storage.getString("accountUuids", "").split(",".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
|
||||
|
||||
// Create a list of all account UUIDs excluding this account
|
||||
val newUuids = ArrayList<String>(uuids.size)
|
||||
for (uuid in uuids) {
|
||||
if (uuid != accountUuid) {
|
||||
newUuids.add(uuid)
|
||||
}
|
||||
}
|
||||
|
||||
// Only change the 'accountUuids' value if this account's UUID was listed before
|
||||
if (newUuids.size < uuids.size) {
|
||||
val accountUuids = Utility.combine(newUuids.toTypedArray(), ',')
|
||||
editor.putString("accountUuids", accountUuids)
|
||||
}
|
||||
|
||||
editor.remove("$accountUuid.$INCOMING_SERVER_SETTINGS_KEY")
|
||||
editor.remove("$accountUuid.$OUTGOING_SERVER_SETTINGS_KEY")
|
||||
editor.remove("$accountUuid.oAuthState")
|
||||
editor.remove("$accountUuid.description")
|
||||
editor.remove("$accountUuid.name")
|
||||
editor.remove("$accountUuid.email")
|
||||
editor.remove("$accountUuid.alwaysBcc")
|
||||
editor.remove("$accountUuid.automaticCheckIntervalMinutes")
|
||||
editor.remove("$accountUuid.idleRefreshMinutes")
|
||||
editor.remove("$accountUuid.lastAutomaticCheckTime")
|
||||
editor.remove("$accountUuid.notifyNewMail")
|
||||
editor.remove("$accountUuid.notifySelfNewMail")
|
||||
editor.remove("$accountUuid.ignoreChatMessages")
|
||||
editor.remove("$accountUuid.messagesNotificationChannelVersion")
|
||||
editor.remove("$accountUuid.deletePolicy")
|
||||
editor.remove("$accountUuid.draftsFolderName")
|
||||
editor.remove("$accountUuid.sentFolderName")
|
||||
editor.remove("$accountUuid.trashFolderName")
|
||||
editor.remove("$accountUuid.archiveFolderName")
|
||||
editor.remove("$accountUuid.spamFolderName")
|
||||
editor.remove("$accountUuid.archiveFolderSelection")
|
||||
editor.remove("$accountUuid.draftsFolderSelection")
|
||||
editor.remove("$accountUuid.sentFolderSelection")
|
||||
editor.remove("$accountUuid.spamFolderSelection")
|
||||
editor.remove("$accountUuid.trashFolderSelection")
|
||||
editor.remove("$accountUuid.autoExpandFolderName")
|
||||
editor.remove("$accountUuid.accountNumber")
|
||||
editor.remove("$accountUuid.vibrate")
|
||||
editor.remove("$accountUuid.vibratePattern")
|
||||
editor.remove("$accountUuid.vibrateTimes")
|
||||
editor.remove("$accountUuid.ring")
|
||||
editor.remove("$accountUuid.ringtone")
|
||||
editor.remove("$accountUuid.folderDisplayMode")
|
||||
editor.remove("$accountUuid.folderSyncMode")
|
||||
editor.remove("$accountUuid.folderPushMode")
|
||||
editor.remove("$accountUuid.folderTargetMode")
|
||||
editor.remove("$accountUuid.signatureBeforeQuotedText")
|
||||
editor.remove("$accountUuid.expungePolicy")
|
||||
editor.remove("$accountUuid.syncRemoteDeletions")
|
||||
editor.remove("$accountUuid.maxPushFolders")
|
||||
editor.remove("$accountUuid.searchableFolders")
|
||||
editor.remove("$accountUuid.chipColor")
|
||||
editor.remove("$accountUuid.notificationLight")
|
||||
editor.remove("$accountUuid.subscribedFoldersOnly")
|
||||
editor.remove("$accountUuid.maximumPolledMessageAge")
|
||||
editor.remove("$accountUuid.maximumAutoDownloadMessageSize")
|
||||
editor.remove("$accountUuid.messageFormatAuto")
|
||||
editor.remove("$accountUuid.quoteStyle")
|
||||
editor.remove("$accountUuid.quotePrefix")
|
||||
editor.remove("$accountUuid.sortTypeEnum")
|
||||
editor.remove("$accountUuid.sortAscending")
|
||||
editor.remove("$accountUuid.showPicturesEnum")
|
||||
editor.remove("$accountUuid.replyAfterQuote")
|
||||
editor.remove("$accountUuid.stripSignature")
|
||||
editor.remove("$accountUuid.cryptoApp") // this is no longer set, but cleans up legacy values
|
||||
editor.remove("$accountUuid.cryptoAutoSignature")
|
||||
editor.remove("$accountUuid.cryptoAutoEncrypt")
|
||||
editor.remove("$accountUuid.cryptoApp")
|
||||
editor.remove("$accountUuid.cryptoKey")
|
||||
editor.remove("$accountUuid.cryptoSupportSignOnly")
|
||||
editor.remove("$accountUuid.openPgpProvider")
|
||||
editor.remove("$accountUuid.openPgpHideSignOnly")
|
||||
editor.remove("$accountUuid.openPgpEncryptSubject")
|
||||
editor.remove("$accountUuid.openPgpEncryptAllDrafts")
|
||||
editor.remove("$accountUuid.autocryptMutualMode")
|
||||
editor.remove("$accountUuid.enabled")
|
||||
editor.remove("$accountUuid.markMessageAsReadOnView")
|
||||
editor.remove("$accountUuid.markMessageAsReadOnDelete")
|
||||
editor.remove("$accountUuid.alwaysShowCcBcc")
|
||||
editor.remove("$accountUuid.remoteSearchFullText")
|
||||
editor.remove("$accountUuid.remoteSearchNumResults")
|
||||
editor.remove("$accountUuid.uploadSentMessages")
|
||||
editor.remove("$accountUuid.defaultQuotedTextShown")
|
||||
editor.remove("$accountUuid.displayCount")
|
||||
editor.remove("$accountUuid.inboxFolderName")
|
||||
editor.remove("$accountUuid.localStorageProvider")
|
||||
editor.remove("$accountUuid.messageFormat")
|
||||
editor.remove("$accountUuid.messageReadReceipt")
|
||||
editor.remove("$accountUuid.notifyMailCheck")
|
||||
editor.remove("$accountUuid.inboxFolderId")
|
||||
editor.remove("$accountUuid.outboxFolderId")
|
||||
editor.remove("$accountUuid.draftsFolderId")
|
||||
editor.remove("$accountUuid.sentFolderId")
|
||||
editor.remove("$accountUuid.trashFolderId")
|
||||
editor.remove("$accountUuid.archiveFolderId")
|
||||
editor.remove("$accountUuid.spamFolderId")
|
||||
editor.remove("$accountUuid.autoExpandFolderId")
|
||||
editor.remove("$accountUuid.lastSyncTime")
|
||||
editor.remove("$accountUuid.lastFolderListRefreshTime")
|
||||
editor.remove("$accountUuid.isFinishedSetup")
|
||||
editor.remove("$accountUuid.useCompression")
|
||||
editor.remove("$accountUuid.migrateToOAuth")
|
||||
|
||||
deleteIdentities(account, storage, editor)
|
||||
// TODO: Remove preference settings that may exist for individual folders in the account.
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
private fun saveIdentities(account: Account, storage: Storage, editor: StorageEditor) {
|
||||
deleteIdentities(account, storage, editor)
|
||||
var ident = 0
|
||||
|
||||
with(account) {
|
||||
for (identity in identities) {
|
||||
editor.putString("$uuid.$IDENTITY_NAME_KEY.$ident", identity.name)
|
||||
editor.putString("$uuid.$IDENTITY_EMAIL_KEY.$ident", identity.email)
|
||||
editor.putBoolean("$uuid.signatureUse.$ident", identity.signatureUse)
|
||||
editor.putString("$uuid.signature.$ident", identity.signature)
|
||||
editor.putString("$uuid.$IDENTITY_DESCRIPTION_KEY.$ident", identity.description)
|
||||
editor.putString("$uuid.replyTo.$ident", identity.replyTo)
|
||||
ident++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
private fun deleteIdentities(account: Account, storage: Storage, editor: StorageEditor) {
|
||||
val accountUuid = account.uuid
|
||||
|
||||
var identityIndex = 0
|
||||
var gotOne: Boolean
|
||||
do {
|
||||
gotOne = false
|
||||
val email = storage.getString("$accountUuid.$IDENTITY_EMAIL_KEY.$identityIndex", null)
|
||||
if (email != null) {
|
||||
editor.remove("$accountUuid.$IDENTITY_NAME_KEY.$identityIndex")
|
||||
editor.remove("$accountUuid.$IDENTITY_EMAIL_KEY.$identityIndex")
|
||||
editor.remove("$accountUuid.signatureUse.$identityIndex")
|
||||
editor.remove("$accountUuid.signature.$identityIndex")
|
||||
editor.remove("$accountUuid.$IDENTITY_DESCRIPTION_KEY.$identityIndex")
|
||||
editor.remove("$accountUuid.replyTo.$identityIndex")
|
||||
gotOne = true
|
||||
}
|
||||
identityIndex++
|
||||
} while (gotOne)
|
||||
}
|
||||
|
||||
fun move(editor: StorageEditor, account: Account, storage: Storage, newPosition: Int) {
|
||||
val accountUuids = storage.getString("accountUuids", "").split(",").filter { it.isNotEmpty() }
|
||||
val oldPosition = accountUuids.indexOf(account.uuid)
|
||||
if (oldPosition == -1 || oldPosition == newPosition) return
|
||||
|
||||
val newAccountUuidsString = accountUuids.toMutableList()
|
||||
.apply {
|
||||
removeAt(oldPosition)
|
||||
add(newPosition, account.uuid)
|
||||
}
|
||||
.joinToString(separator = ",")
|
||||
|
||||
editor.putString("accountUuids", newAccountUuidsString)
|
||||
}
|
||||
|
||||
private fun <T : Enum<T>> getEnumStringPref(storage: Storage, key: String, defaultEnum: T): T {
|
||||
val stringPref = storage.getString(key, null)
|
||||
|
||||
return if (stringPref == null) {
|
||||
defaultEnum
|
||||
} else {
|
||||
try {
|
||||
java.lang.Enum.valueOf<T>(defaultEnum.declaringJavaClass, stringPref)
|
||||
} catch (ex: IllegalArgumentException) {
|
||||
Timber.w(
|
||||
ex,
|
||||
"Unable to convert preference key [%s] value [%s] to enum of type %s",
|
||||
key,
|
||||
stringPref,
|
||||
defaultEnum.declaringJavaClass
|
||||
)
|
||||
|
||||
defaultEnum
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun loadDefaults(account: Account) {
|
||||
with(account) {
|
||||
localStorageProviderId = storageManager.defaultProviderId
|
||||
automaticCheckIntervalMinutes = DEFAULT_SYNC_INTERVAL
|
||||
idleRefreshMinutes = 24
|
||||
displayCount = K9.DEFAULT_VISIBLE_LIMIT
|
||||
accountNumber = UNASSIGNED_ACCOUNT_NUMBER
|
||||
isNotifyNewMail = true
|
||||
folderNotifyNewMailMode = FolderMode.ALL
|
||||
isNotifySync = false
|
||||
isNotifySelfNewMail = true
|
||||
isNotifyContactsMailOnly = false
|
||||
isIgnoreChatMessages = false
|
||||
messagesNotificationChannelVersion = 0
|
||||
folderDisplayMode = FolderMode.NOT_SECOND_CLASS
|
||||
folderSyncMode = FolderMode.FIRST_CLASS
|
||||
folderPushMode = FolderMode.NONE
|
||||
folderTargetMode = FolderMode.NOT_SECOND_CLASS
|
||||
sortType = DEFAULT_SORT_TYPE
|
||||
setSortAscending(DEFAULT_SORT_TYPE, DEFAULT_SORT_ASCENDING)
|
||||
showPictures = ShowPictures.NEVER
|
||||
isSignatureBeforeQuotedText = false
|
||||
expungePolicy = Expunge.EXPUNGE_IMMEDIATELY
|
||||
importedAutoExpandFolder = null
|
||||
legacyInboxFolder = null
|
||||
maxPushFolders = 10
|
||||
isSubscribedFoldersOnly = false
|
||||
maximumPolledMessageAge = -1
|
||||
maximumAutoDownloadMessageSize = 32768
|
||||
messageFormat = DEFAULT_MESSAGE_FORMAT
|
||||
isMessageFormatAuto = DEFAULT_MESSAGE_FORMAT_AUTO
|
||||
isMessageReadReceipt = DEFAULT_MESSAGE_READ_RECEIPT
|
||||
quoteStyle = DEFAULT_QUOTE_STYLE
|
||||
quotePrefix = DEFAULT_QUOTE_PREFIX
|
||||
isDefaultQuotedTextShown = DEFAULT_QUOTED_TEXT_SHOWN
|
||||
isReplyAfterQuote = DEFAULT_REPLY_AFTER_QUOTE
|
||||
isStripSignature = DEFAULT_STRIP_SIGNATURE
|
||||
isSyncRemoteDeletions = true
|
||||
openPgpKey = NO_OPENPGP_KEY
|
||||
isRemoteSearchFullText = false
|
||||
remoteSearchNumResults = DEFAULT_REMOTE_SEARCH_NUM_RESULTS
|
||||
isUploadSentMessages = true
|
||||
isMarkMessageAsReadOnView = true
|
||||
isMarkMessageAsReadOnDelete = true
|
||||
isAlwaysShowCcBcc = false
|
||||
lastSyncTime = 0L
|
||||
lastFolderListRefreshTime = 0L
|
||||
|
||||
setArchiveFolderId(null, SpecialFolderSelection.AUTOMATIC)
|
||||
setDraftsFolderId(null, SpecialFolderSelection.AUTOMATIC)
|
||||
setSentFolderId(null, SpecialFolderSelection.AUTOMATIC)
|
||||
setSpamFolderId(null, SpecialFolderSelection.AUTOMATIC)
|
||||
setTrashFolderId(null, SpecialFolderSelection.AUTOMATIC)
|
||||
setArchiveFolderId(null, SpecialFolderSelection.AUTOMATIC)
|
||||
|
||||
searchableFolders = Searchable.ALL
|
||||
|
||||
identities = ArrayList<Identity>()
|
||||
|
||||
val identity = Identity(
|
||||
signatureUse = false,
|
||||
signature = resourceProvider.defaultSignature(),
|
||||
description = resourceProvider.defaultIdentityDescription()
|
||||
)
|
||||
identities.add(identity)
|
||||
|
||||
updateNotificationSettings {
|
||||
NotificationSettings(
|
||||
isRingEnabled = true,
|
||||
ringtone = DEFAULT_RINGTONE_URI,
|
||||
light = NotificationLight.Disabled,
|
||||
vibration = NotificationVibration.DEFAULT
|
||||
)
|
||||
}
|
||||
|
||||
resetChangeMarkers()
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val ACCOUNT_DESCRIPTION_KEY = "description"
|
||||
const val INCOMING_SERVER_SETTINGS_KEY = "incomingServerSettings"
|
||||
const val OUTGOING_SERVER_SETTINGS_KEY = "outgoingServerSettings"
|
||||
|
||||
const val IDENTITY_NAME_KEY = "name"
|
||||
const val IDENTITY_EMAIL_KEY = "email"
|
||||
const val IDENTITY_DESCRIPTION_KEY = "description"
|
||||
|
||||
const val FALLBACK_ACCOUNT_COLOR = 0x0099CC
|
||||
|
||||
@JvmField
|
||||
val DEFAULT_MESSAGE_FORMAT = MessageFormat.HTML
|
||||
|
||||
@JvmField
|
||||
val DEFAULT_QUOTE_STYLE = QuoteStyle.PREFIX
|
||||
const val DEFAULT_MESSAGE_FORMAT_AUTO = false
|
||||
const val DEFAULT_MESSAGE_READ_RECEIPT = false
|
||||
const val DEFAULT_QUOTE_PREFIX = ">"
|
||||
const val DEFAULT_QUOTED_TEXT_SHOWN = true
|
||||
const val DEFAULT_REPLY_AFTER_QUOTE = false
|
||||
const val DEFAULT_STRIP_SIGNATURE = true
|
||||
const val DEFAULT_REMOTE_SEARCH_NUM_RESULTS = 25
|
||||
const val DEFAULT_RINGTONE_URI = "content://settings/system/notification_sound"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
package com.fsck.k9
|
||||
|
||||
fun interface AccountRemovedListener {
|
||||
fun onAccountRemoved(account: Account)
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
package com.fsck.k9;
|
||||
|
||||
|
||||
public interface AccountsChangeListener {
|
||||
void onAccountsChanged();
|
||||
}
|
||||
11
app/core/src/main/java/com/fsck/k9/ActivityExtensions.kt
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
package com.fsck.k9
|
||||
|
||||
import android.app.Activity
|
||||
import android.widget.Toast
|
||||
import androidx.annotation.StringRes
|
||||
|
||||
fun Activity.finishWithErrorToast(@StringRes errorRes: Int, vararg formatArgs: String) {
|
||||
val text = getString(errorRes, *formatArgs)
|
||||
Toast.makeText(this, text, Toast.LENGTH_LONG).show()
|
||||
finish()
|
||||
}
|
||||
5
app/core/src/main/java/com/fsck/k9/AppConfig.kt
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
package com.fsck.k9
|
||||
|
||||
data class AppConfig(
|
||||
val componentsToDisable: List<Class<*>>
|
||||
)
|
||||
7
app/core/src/main/java/com/fsck/k9/BaseAccount.kt
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
package com.fsck.k9
|
||||
|
||||
interface BaseAccount {
|
||||
val uuid: String
|
||||
val name: String?
|
||||
val email: String
|
||||
}
|
||||
89
app/core/src/main/java/com/fsck/k9/Core.kt
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
package com.fsck.k9
|
||||
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageManager
|
||||
import com.fsck.k9.job.K9JobManager
|
||||
import com.fsck.k9.mail.internet.BinaryTempFileBody
|
||||
import com.fsck.k9.notification.NotificationController
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import org.koin.core.qualifier.named
|
||||
|
||||
object Core : EarlyInit {
|
||||
private val context: Context by inject()
|
||||
private val appConfig: AppConfig by inject()
|
||||
private val jobManager: K9JobManager by inject()
|
||||
private val appCoroutineScope: CoroutineScope by inject(named("AppCoroutineScope"))
|
||||
private val preferences: Preferences by inject()
|
||||
private val notificationController: NotificationController by inject()
|
||||
|
||||
/**
|
||||
* This needs to be called from [Application#onCreate][android.app.Application#onCreate] before calling through
|
||||
* to the super class's `onCreate` implementation and before initializing the dependency injection library.
|
||||
*/
|
||||
fun earlyInit() {
|
||||
if (K9.DEVELOPER_MODE) {
|
||||
enableStrictMode()
|
||||
}
|
||||
}
|
||||
|
||||
fun init(context: Context) {
|
||||
BinaryTempFileBody.setTempDirectory(context.cacheDir)
|
||||
|
||||
setServicesEnabled(context)
|
||||
|
||||
restoreNotifications()
|
||||
}
|
||||
|
||||
/**
|
||||
* Called throughout the application when the number of accounts has changed. This method
|
||||
* enables or disables the Compose activity, the boot receiver and the service based on
|
||||
* whether any accounts are configured.
|
||||
*/
|
||||
@JvmStatic
|
||||
fun setServicesEnabled(context: Context) {
|
||||
val appContext = context.applicationContext
|
||||
val acctLength = Preferences.getPreferences().accounts.size
|
||||
val enable = acctLength > 0
|
||||
|
||||
setServicesEnabled(appContext, enable)
|
||||
}
|
||||
|
||||
fun setServicesEnabled() {
|
||||
setServicesEnabled(context)
|
||||
}
|
||||
|
||||
private fun setServicesEnabled(context: Context, enabled: Boolean) {
|
||||
val pm = context.packageManager
|
||||
|
||||
for (clazz in appConfig.componentsToDisable) {
|
||||
val alreadyEnabled = pm.getComponentEnabledSetting(ComponentName(context, clazz)) ==
|
||||
PackageManager.COMPONENT_ENABLED_STATE_ENABLED
|
||||
|
||||
if (enabled != alreadyEnabled) {
|
||||
pm.setComponentEnabledSetting(
|
||||
ComponentName(context, clazz),
|
||||
if (enabled) {
|
||||
PackageManager.COMPONENT_ENABLED_STATE_ENABLED
|
||||
} else {
|
||||
PackageManager.COMPONENT_ENABLED_STATE_DISABLED
|
||||
},
|
||||
PackageManager.DONT_KILL_APP
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (enabled) {
|
||||
jobManager.scheduleAllMailJobs()
|
||||
}
|
||||
}
|
||||
|
||||
private fun restoreNotifications() {
|
||||
appCoroutineScope.launch(Dispatchers.IO) {
|
||||
val accounts = preferences.accounts
|
||||
notificationController.restoreNewMailNotifications(accounts)
|
||||
}
|
||||
}
|
||||
}
|
||||
36
app/core/src/main/java/com/fsck/k9/CoreKoinModules.kt
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
package com.fsck.k9
|
||||
|
||||
import com.fsck.k9.autocrypt.autocryptModule
|
||||
import com.fsck.k9.controller.controllerModule
|
||||
import com.fsck.k9.controller.push.controllerPushModule
|
||||
import com.fsck.k9.crypto.openPgpModule
|
||||
import com.fsck.k9.helper.helperModule
|
||||
import com.fsck.k9.job.jobModule
|
||||
import com.fsck.k9.logging.loggingModule
|
||||
import com.fsck.k9.mailstore.mailStoreModule
|
||||
import com.fsck.k9.message.extractors.extractorModule
|
||||
import com.fsck.k9.message.html.htmlModule
|
||||
import com.fsck.k9.message.quote.quoteModule
|
||||
import com.fsck.k9.network.connectivityModule
|
||||
import com.fsck.k9.notification.coreNotificationModule
|
||||
import com.fsck.k9.power.powerModule
|
||||
import com.fsck.k9.preferences.preferencesModule
|
||||
|
||||
val coreModules = listOf(
|
||||
mainModule,
|
||||
openPgpModule,
|
||||
autocryptModule,
|
||||
mailStoreModule,
|
||||
extractorModule,
|
||||
htmlModule,
|
||||
quoteModule,
|
||||
coreNotificationModule,
|
||||
controllerModule,
|
||||
controllerPushModule,
|
||||
jobModule,
|
||||
helperModule,
|
||||
preferencesModule,
|
||||
connectivityModule,
|
||||
powerModule,
|
||||
loggingModule
|
||||
)
|
||||
35
app/core/src/main/java/com/fsck/k9/CoreResourceProvider.kt
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
package com.fsck.k9
|
||||
|
||||
import com.fsck.k9.notification.PushNotificationState
|
||||
|
||||
interface CoreResourceProvider {
|
||||
fun defaultSignature(): String
|
||||
fun defaultIdentityDescription(): String
|
||||
|
||||
fun contactDisplayNamePrefix(): String
|
||||
fun contactUnknownSender(): String
|
||||
fun contactUnknownRecipient(): String
|
||||
|
||||
fun messageHeaderFrom(): String
|
||||
fun messageHeaderTo(): String
|
||||
fun messageHeaderCc(): String
|
||||
fun messageHeaderDate(): String
|
||||
fun messageHeaderSubject(): String
|
||||
fun messageHeaderSeparator(): String
|
||||
|
||||
fun noSubject(): String
|
||||
fun userAgent(): String
|
||||
|
||||
fun encryptedSubject(): String
|
||||
fun replyHeader(sender: String): String
|
||||
fun replyHeader(sender: String, sentDate: String): String
|
||||
|
||||
fun searchUnifiedInboxTitle(): String
|
||||
fun searchUnifiedInboxDetail(): String
|
||||
|
||||
fun outboxFolderName(): String
|
||||
|
||||
val iconPushNotification: Int
|
||||
fun pushNotificationText(notificationState: PushNotificationState): String
|
||||
fun pushNotificationInfoText(): String
|
||||
}
|
||||
44
app/core/src/main/java/com/fsck/k9/DI.kt
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
package com.fsck.k9
|
||||
|
||||
import android.app.Application
|
||||
import com.fsck.k9.core.BuildConfig
|
||||
import org.koin.android.ext.koin.androidContext
|
||||
import org.koin.android.ext.koin.androidLogger
|
||||
import org.koin.core.context.startKoin
|
||||
import org.koin.core.module.Module
|
||||
import org.koin.core.parameter.ParametersDefinition
|
||||
import org.koin.core.qualifier.Qualifier
|
||||
import org.koin.java.KoinJavaComponent.getKoin
|
||||
import org.koin.java.KoinJavaComponent.get as koinGet
|
||||
|
||||
object DI {
|
||||
private const val DEBUG = false
|
||||
|
||||
@JvmStatic fun start(application: Application, modules: List<Module>) {
|
||||
startKoin {
|
||||
if (BuildConfig.DEBUG && DEBUG) {
|
||||
androidLogger()
|
||||
}
|
||||
|
||||
androidContext(application)
|
||||
modules(modules)
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun <T : Any> get(clazz: Class<T>): T {
|
||||
return koinGet(clazz)
|
||||
}
|
||||
|
||||
inline fun <reified T : Any> get(): T {
|
||||
return koinGet(T::class.java)
|
||||
}
|
||||
}
|
||||
|
||||
interface EarlyInit
|
||||
|
||||
// Copied from ComponentCallbacks.inject()
|
||||
inline fun <reified T : Any> EarlyInit.inject(
|
||||
qualifier: Qualifier? = null,
|
||||
noinline parameters: ParametersDefinition? = null
|
||||
) = lazy { getKoin().get<T>(qualifier, parameters) }
|
||||
27
app/core/src/main/java/com/fsck/k9/EmailAddressValidator.kt
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
package com.fsck.k9
|
||||
|
||||
import java.util.regex.Pattern
|
||||
|
||||
class EmailAddressValidator {
|
||||
|
||||
fun isValidAddressOnly(text: CharSequence): Boolean = EMAIL_ADDRESS_PATTERN.matcher(text).matches()
|
||||
|
||||
companion object {
|
||||
|
||||
// https://www.rfc-editor.org/rfc/rfc2396.txt (3.2.2)
|
||||
// https://www.rfc-editor.org/rfc/rfc5321.txt (4.1.2)
|
||||
|
||||
private const val ALPHA = "[a-zA-Z]"
|
||||
private const val ALPHANUM = "[a-zA-Z0-9]"
|
||||
private const val ATEXT = "[0-9a-zA-Z!#$%&'*+\\-/=?^_`{|}~]"
|
||||
private const val QCONTENT = "([\\p{Graph}\\p{Blank}&&[^\"\\\\]]|\\\\[\\p{Graph}\\p{Blank}])"
|
||||
private const val TOP_LABEL = "(($ALPHA($ALPHANUM|\\-|_)*$ALPHANUM)|$ALPHA)"
|
||||
private const val DOMAIN_LABEL = "(($ALPHANUM($ALPHANUM|\\-|_)*$ALPHANUM)|$ALPHANUM)"
|
||||
private const val HOST_NAME = "((($DOMAIN_LABEL\\.)+$TOP_LABEL)|$DOMAIN_LABEL)"
|
||||
|
||||
private val EMAIL_ADDRESS_PATTERN = Pattern.compile(
|
||||
"^($ATEXT+(\\.$ATEXT+)*|\"$QCONTENT+\")" +
|
||||
"\\@$HOST_NAME"
|
||||
)
|
||||
}
|
||||
}
|
||||
197
app/core/src/main/java/com/fsck/k9/FontSizes.java
Normal file
|
|
@ -0,0 +1,197 @@
|
|||
package com.fsck.k9;
|
||||
|
||||
import android.util.TypedValue;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.fsck.k9.preferences.Storage;
|
||||
import com.fsck.k9.preferences.StorageEditor;
|
||||
|
||||
|
||||
/**
|
||||
* Manage font size of the information displayed in the message list and in the message view.
|
||||
*/
|
||||
public class FontSizes {
|
||||
private static final String MESSAGE_LIST_SUBJECT = "fontSizeMessageListSubject";
|
||||
private static final String MESSAGE_LIST_SENDER = "fontSizeMessageListSender";
|
||||
private static final String MESSAGE_LIST_DATE = "fontSizeMessageListDate";
|
||||
private static final String MESSAGE_LIST_PREVIEW = "fontSizeMessageListPreview";
|
||||
private static final String MESSAGE_VIEW_ACCOUNT_NAME = "fontSizeMessageViewAccountName";
|
||||
private static final String MESSAGE_VIEW_SENDER = "fontSizeMessageViewSender";
|
||||
private static final String MESSAGE_VIEW_RECIPIENTS = "fontSizeMessageViewTo";
|
||||
private static final String MESSAGE_VIEW_SUBJECT = "fontSizeMessageViewSubject";
|
||||
private static final String MESSAGE_VIEW_DATE = "fontSizeMessageViewDate";
|
||||
private static final String MESSAGE_VIEW_CONTENT_PERCENT = "fontSizeMessageViewContentPercent";
|
||||
private static final String MESSAGE_COMPOSE_INPUT = "fontSizeMessageComposeInput";
|
||||
|
||||
public static final int FONT_DEFAULT = -1; // Don't force-reset the size of this setting
|
||||
public static final int FONT_10SP = 10;
|
||||
public static final int FONT_12SP = 12;
|
||||
public static final int SMALL = 14; // ?android:attr/textAppearanceSmall
|
||||
public static final int FONT_16SP = 16;
|
||||
public static final int MEDIUM = 18; // ?android:attr/textAppearanceMedium
|
||||
public static final int FONT_20SP = 20;
|
||||
public static final int LARGE = 22; // ?android:attr/textAppearanceLarge
|
||||
|
||||
|
||||
private int messageListSubject;
|
||||
private int messageListSender;
|
||||
private int messageListDate;
|
||||
private int messageListPreview;
|
||||
private int messageViewAccountName;
|
||||
private int messageViewSender;
|
||||
private int messageViewRecipients;
|
||||
private int messageViewSubject;
|
||||
private int messageViewDate;
|
||||
private int messageViewContentPercent;
|
||||
private int messageComposeInput;
|
||||
|
||||
|
||||
public FontSizes() {
|
||||
messageListSubject = FONT_DEFAULT;
|
||||
messageListSender = FONT_DEFAULT;
|
||||
messageListDate = FONT_DEFAULT;
|
||||
messageListPreview = FONT_DEFAULT;
|
||||
|
||||
messageViewAccountName = FONT_DEFAULT;
|
||||
messageViewSender = FONT_DEFAULT;
|
||||
messageViewRecipients = FONT_DEFAULT;
|
||||
messageViewSubject = FONT_DEFAULT;
|
||||
messageViewDate = FONT_DEFAULT;
|
||||
messageViewContentPercent = 100;
|
||||
|
||||
messageComposeInput = MEDIUM;
|
||||
}
|
||||
|
||||
public void save(StorageEditor editor) {
|
||||
editor.putInt(MESSAGE_LIST_SUBJECT, messageListSubject);
|
||||
editor.putInt(MESSAGE_LIST_SENDER, messageListSender);
|
||||
editor.putInt(MESSAGE_LIST_DATE, messageListDate);
|
||||
editor.putInt(MESSAGE_LIST_PREVIEW, messageListPreview);
|
||||
|
||||
editor.putInt(MESSAGE_VIEW_ACCOUNT_NAME, messageViewAccountName);
|
||||
editor.putInt(MESSAGE_VIEW_SENDER, messageViewSender);
|
||||
editor.putInt(MESSAGE_VIEW_RECIPIENTS, messageViewRecipients);
|
||||
editor.putInt(MESSAGE_VIEW_SUBJECT, messageViewSubject);
|
||||
editor.putInt(MESSAGE_VIEW_DATE, messageViewDate);
|
||||
editor.putInt(MESSAGE_VIEW_CONTENT_PERCENT, getMessageViewContentAsPercent());
|
||||
|
||||
editor.putInt(MESSAGE_COMPOSE_INPUT, messageComposeInput);
|
||||
}
|
||||
|
||||
public void load(Storage storage) {
|
||||
messageListSubject = storage.getInt(MESSAGE_LIST_SUBJECT, messageListSubject);
|
||||
messageListSender = storage.getInt(MESSAGE_LIST_SENDER, messageListSender);
|
||||
messageListDate = storage.getInt(MESSAGE_LIST_DATE, messageListDate);
|
||||
messageListPreview = storage.getInt(MESSAGE_LIST_PREVIEW, messageListPreview);
|
||||
|
||||
messageViewAccountName = storage.getInt(MESSAGE_VIEW_ACCOUNT_NAME, messageViewAccountName);
|
||||
messageViewSender = storage.getInt(MESSAGE_VIEW_SENDER, messageViewSender);
|
||||
messageViewRecipients = storage.getInt(MESSAGE_VIEW_RECIPIENTS, messageViewRecipients);
|
||||
messageViewSubject = storage.getInt(MESSAGE_VIEW_SUBJECT, messageViewSubject);
|
||||
messageViewDate = storage.getInt(MESSAGE_VIEW_DATE, messageViewDate);
|
||||
|
||||
loadMessageViewContentPercent(storage);
|
||||
|
||||
messageComposeInput = storage.getInt(MESSAGE_COMPOSE_INPUT, messageComposeInput);
|
||||
}
|
||||
|
||||
private void loadMessageViewContentPercent(Storage storage) {
|
||||
setMessageViewContentAsPercent(storage.getInt(MESSAGE_VIEW_CONTENT_PERCENT, 100));
|
||||
}
|
||||
|
||||
public int getMessageListSubject() {
|
||||
return messageListSubject;
|
||||
}
|
||||
|
||||
public void setMessageListSubject(int messageListSubject) {
|
||||
this.messageListSubject = messageListSubject;
|
||||
}
|
||||
|
||||
public int getMessageListSender() {
|
||||
return messageListSender;
|
||||
}
|
||||
|
||||
public void setMessageListSender(int messageListSender) {
|
||||
this.messageListSender = messageListSender;
|
||||
}
|
||||
|
||||
public int getMessageListDate() {
|
||||
return messageListDate;
|
||||
}
|
||||
|
||||
public void setMessageListDate(int messageListDate) {
|
||||
this.messageListDate = messageListDate;
|
||||
}
|
||||
|
||||
public int getMessageListPreview() {
|
||||
return messageListPreview;
|
||||
}
|
||||
|
||||
public void setMessageListPreview(int messageListPreview) {
|
||||
this.messageListPreview = messageListPreview;
|
||||
}
|
||||
|
||||
public int getMessageViewAccountName() {
|
||||
return messageViewAccountName;
|
||||
}
|
||||
|
||||
public void setMessageViewAccountName(int messageViewAccountName) {
|
||||
this.messageViewAccountName = messageViewAccountName;
|
||||
}
|
||||
|
||||
public int getMessageViewSender() {
|
||||
return messageViewSender;
|
||||
}
|
||||
|
||||
public void setMessageViewSender(int messageViewSender) {
|
||||
this.messageViewSender = messageViewSender;
|
||||
}
|
||||
|
||||
public int getMessageViewRecipients() {
|
||||
return messageViewRecipients;
|
||||
}
|
||||
|
||||
public void setMessageViewRecipients(int messageViewRecipients) {
|
||||
this.messageViewRecipients = messageViewRecipients;
|
||||
}
|
||||
|
||||
public int getMessageViewSubject() {
|
||||
return messageViewSubject;
|
||||
}
|
||||
|
||||
public void setMessageViewSubject(int messageViewSubject) {
|
||||
this.messageViewSubject = messageViewSubject;
|
||||
}
|
||||
|
||||
public int getMessageViewDate() {
|
||||
return messageViewDate;
|
||||
}
|
||||
|
||||
public void setMessageViewDate(int messageViewDate) {
|
||||
this.messageViewDate = messageViewDate;
|
||||
}
|
||||
|
||||
public int getMessageViewContentAsPercent() {
|
||||
return messageViewContentPercent;
|
||||
}
|
||||
|
||||
public void setMessageViewContentAsPercent(int size) {
|
||||
messageViewContentPercent = size;
|
||||
}
|
||||
|
||||
public int getMessageComposeInput() {
|
||||
return messageComposeInput;
|
||||
}
|
||||
|
||||
public void setMessageComposeInput(int messageComposeInput) {
|
||||
this.messageComposeInput = messageComposeInput;
|
||||
}
|
||||
|
||||
// This, arguably, should live somewhere in a view class, but since we call it from activities, fragments
|
||||
// and views, where isn't exactly clear.
|
||||
public void setViewTextSize(TextView v, int fontSize) {
|
||||
if (fontSize != FONT_DEFAULT) {
|
||||
v.setTextSize(TypedValue.COMPLEX_UNIT_SP, fontSize);
|
||||
}
|
||||
}
|
||||
}
|
||||
20
app/core/src/main/java/com/fsck/k9/Identity.kt
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
package com.fsck.k9
|
||||
|
||||
import android.os.Parcelable
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@Parcelize
|
||||
data class Identity(
|
||||
val description: String? = null,
|
||||
val name: String? = null,
|
||||
val email: String? = null,
|
||||
val signature: String? = null,
|
||||
val signatureUse: Boolean = false,
|
||||
val replyTo: String? = null
|
||||
) : Parcelable {
|
||||
// TODO remove when callers are converted to Kotlin
|
||||
fun withName(name: String?) = copy(name = name)
|
||||
fun withSignature(signature: String?) = copy(signature = signature)
|
||||
fun withSignatureUse(signatureUse: Boolean) = copy(signatureUse = signatureUse)
|
||||
fun withEmail(email: String?) = copy(email = email)
|
||||
}
|
||||
535
app/core/src/main/java/com/fsck/k9/K9.kt
Normal file
|
|
@ -0,0 +1,535 @@
|
|||
package com.fsck.k9
|
||||
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import com.fsck.k9.Account.SortType
|
||||
import com.fsck.k9.core.BuildConfig
|
||||
import com.fsck.k9.mail.K9MailLib
|
||||
import com.fsck.k9.mailstore.LocalStore
|
||||
import com.fsck.k9.preferences.RealGeneralSettingsManager
|
||||
import com.fsck.k9.preferences.Storage
|
||||
import com.fsck.k9.preferences.StorageEditor
|
||||
import kotlinx.datetime.Clock
|
||||
import timber.log.Timber
|
||||
import timber.log.Timber.DebugTree
|
||||
|
||||
@Deprecated("Use GeneralSettingsManager and GeneralSettings instead")
|
||||
object K9 : EarlyInit {
|
||||
private val generalSettingsManager: RealGeneralSettingsManager by inject()
|
||||
|
||||
/**
|
||||
* If this is `true`, various development settings will be enabled.
|
||||
*/
|
||||
@JvmField
|
||||
val DEVELOPER_MODE = BuildConfig.DEBUG
|
||||
|
||||
/**
|
||||
* Name of the [SharedPreferences] file used to store the last known version of the
|
||||
* accounts' databases.
|
||||
*
|
||||
* See `UpgradeDatabases` for a detailed explanation of the database upgrade process.
|
||||
*/
|
||||
private const val DATABASE_VERSION_CACHE = "database_version_cache"
|
||||
|
||||
/**
|
||||
* Key used to store the last known database version of the accounts' databases.
|
||||
*
|
||||
* @see DATABASE_VERSION_CACHE
|
||||
*/
|
||||
private const val KEY_LAST_ACCOUNT_DATABASE_VERSION = "last_account_database_version"
|
||||
|
||||
/**
|
||||
* A reference to the [SharedPreferences] used for caching the last known database version.
|
||||
*
|
||||
* @see checkCachedDatabaseVersion
|
||||
* @see setDatabasesUpToDate
|
||||
*/
|
||||
private var databaseVersionCache: SharedPreferences? = null
|
||||
|
||||
/**
|
||||
* @see areDatabasesUpToDate
|
||||
*/
|
||||
private var databasesUpToDate = false
|
||||
|
||||
/**
|
||||
* Check if we already know whether all databases are using the current database schema.
|
||||
*
|
||||
* This method is only used for optimizations. If it returns `true` we can be certain that getting a [LocalStore]
|
||||
* instance won't trigger a schema upgrade.
|
||||
*
|
||||
* @return `true`, if we know that all databases are using the current database schema. `false`, otherwise.
|
||||
*/
|
||||
@Synchronized
|
||||
@JvmStatic
|
||||
fun areDatabasesUpToDate(): Boolean {
|
||||
return databasesUpToDate
|
||||
}
|
||||
|
||||
/**
|
||||
* Remember that all account databases are using the most recent database schema.
|
||||
*
|
||||
* @param save
|
||||
* Whether or not to write the current database version to the
|
||||
* `SharedPreferences` [.DATABASE_VERSION_CACHE].
|
||||
*
|
||||
* @see .areDatabasesUpToDate
|
||||
*/
|
||||
@Synchronized
|
||||
@JvmStatic
|
||||
fun setDatabasesUpToDate(save: Boolean) {
|
||||
databasesUpToDate = true
|
||||
|
||||
if (save) {
|
||||
val editor = databaseVersionCache!!.edit()
|
||||
editor.putInt(KEY_LAST_ACCOUNT_DATABASE_VERSION, LocalStore.getDbVersion())
|
||||
editor.apply()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the last known database version of the accounts' databases from a `SharedPreference`.
|
||||
*
|
||||
* If the stored version matches [LocalStore.getDbVersion] we know that the databases are up to date.
|
||||
* Using `SharedPreferences` should be a lot faster than opening all SQLite databases to get the current database
|
||||
* version.
|
||||
*
|
||||
* See the class `UpgradeDatabases` for a detailed explanation of the database upgrade process.
|
||||
*
|
||||
* @see areDatabasesUpToDate
|
||||
*/
|
||||
private fun checkCachedDatabaseVersion(context: Context) {
|
||||
databaseVersionCache = context.getSharedPreferences(DATABASE_VERSION_CACHE, Context.MODE_PRIVATE)
|
||||
|
||||
val cachedVersion = databaseVersionCache!!.getInt(KEY_LAST_ACCOUNT_DATABASE_VERSION, 0)
|
||||
|
||||
if (cachedVersion >= LocalStore.getDbVersion()) {
|
||||
setDatabasesUpToDate(false)
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
var isDebugLoggingEnabled: Boolean = DEVELOPER_MODE
|
||||
set(debug) {
|
||||
field = debug
|
||||
updateLoggingStatus()
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
var isSensitiveDebugLoggingEnabled: Boolean = false
|
||||
|
||||
@JvmStatic
|
||||
var k9Language = ""
|
||||
|
||||
@JvmStatic
|
||||
val fontSizes = FontSizes()
|
||||
|
||||
@JvmStatic
|
||||
var backgroundOps = BACKGROUND_OPS.ALWAYS
|
||||
|
||||
@JvmStatic
|
||||
var isShowAnimations = true
|
||||
|
||||
@JvmStatic
|
||||
var isConfirmDelete = false
|
||||
|
||||
@JvmStatic
|
||||
var isConfirmDiscardMessage = true
|
||||
|
||||
@JvmStatic
|
||||
var isConfirmDeleteStarred = false
|
||||
|
||||
@JvmStatic
|
||||
var isConfirmSpam = false
|
||||
|
||||
@JvmStatic
|
||||
var isConfirmDeleteFromNotification = true
|
||||
|
||||
@JvmStatic
|
||||
var isConfirmMarkAllRead = true
|
||||
|
||||
@JvmStatic
|
||||
var notificationQuickDeleteBehaviour = NotificationQuickDelete.ALWAYS
|
||||
|
||||
@JvmStatic
|
||||
var lockScreenNotificationVisibility = LockScreenNotificationVisibility.MESSAGE_COUNT
|
||||
|
||||
@JvmStatic
|
||||
var messageListDensity: UiDensity = UiDensity.Default
|
||||
|
||||
@JvmStatic
|
||||
var isShowMessageListStars = true
|
||||
|
||||
@JvmStatic
|
||||
var messageListPreviewLines = 2
|
||||
|
||||
@JvmStatic
|
||||
var isShowCorrespondentNames = true
|
||||
|
||||
@JvmStatic
|
||||
var isMessageListSenderAboveSubject = false
|
||||
|
||||
@JvmStatic
|
||||
var isShowContactName = false
|
||||
|
||||
@JvmStatic
|
||||
var isChangeContactNameColor = false
|
||||
|
||||
@JvmStatic
|
||||
var contactNameColor = 0xFF1093F5.toInt()
|
||||
|
||||
@JvmStatic
|
||||
var isShowContactPicture = true
|
||||
|
||||
@JvmStatic
|
||||
var isUseMessageViewFixedWidthFont = false
|
||||
|
||||
@JvmStatic
|
||||
var isMessageViewReturnToList = false
|
||||
|
||||
@JvmStatic
|
||||
var isMessageViewShowNext = false
|
||||
|
||||
@JvmStatic
|
||||
var isUseVolumeKeysForNavigation = false
|
||||
|
||||
@JvmStatic
|
||||
var isShowUnifiedInbox = true
|
||||
|
||||
@JvmStatic
|
||||
var isShowStarredCount = false
|
||||
|
||||
@JvmStatic
|
||||
var isAutoFitWidth: Boolean = false
|
||||
|
||||
var isQuietTimeEnabled = false
|
||||
var isNotificationDuringQuietTimeEnabled = true
|
||||
var quietTimeStarts: String? = null
|
||||
var quietTimeEnds: String? = null
|
||||
|
||||
@JvmStatic
|
||||
var isHideUserAgent = false
|
||||
|
||||
@JvmStatic
|
||||
var isHideTimeZone = false
|
||||
|
||||
@get:Synchronized
|
||||
@set:Synchronized
|
||||
@JvmStatic
|
||||
var sortType: SortType = Account.DEFAULT_SORT_TYPE
|
||||
private val sortAscending = mutableMapOf<SortType, Boolean>()
|
||||
|
||||
@JvmStatic
|
||||
var isUseBackgroundAsUnreadIndicator = false
|
||||
|
||||
@get:Synchronized
|
||||
@set:Synchronized
|
||||
var isShowComposeButtonOnMessageList = true
|
||||
|
||||
@get:Synchronized
|
||||
@set:Synchronized
|
||||
@JvmStatic
|
||||
var isThreadedViewEnabled = true
|
||||
|
||||
@get:Synchronized
|
||||
@set:Synchronized
|
||||
@JvmStatic
|
||||
var splitViewMode = SplitViewMode.NEVER
|
||||
|
||||
var isColorizeMissingContactPictures = true
|
||||
|
||||
@JvmStatic
|
||||
var isMessageViewArchiveActionVisible = false
|
||||
|
||||
@JvmStatic
|
||||
var isMessageViewDeleteActionVisible = true
|
||||
|
||||
@JvmStatic
|
||||
var isMessageViewMoveActionVisible = false
|
||||
|
||||
@JvmStatic
|
||||
var isMessageViewCopyActionVisible = false
|
||||
|
||||
@JvmStatic
|
||||
var isMessageViewSpamActionVisible = false
|
||||
|
||||
@JvmStatic
|
||||
var pgpInlineDialogCounter: Int = 0
|
||||
|
||||
@JvmStatic
|
||||
var pgpSignOnlyDialogCounter: Int = 0
|
||||
|
||||
@JvmStatic
|
||||
var swipeRightAction: SwipeAction = SwipeAction.ToggleSelection
|
||||
|
||||
@JvmStatic
|
||||
var swipeLeftAction: SwipeAction = SwipeAction.ToggleRead
|
||||
|
||||
val isQuietTime: Boolean
|
||||
get() {
|
||||
if (!isQuietTimeEnabled) {
|
||||
return false
|
||||
}
|
||||
|
||||
val clock = DI.get<Clock>()
|
||||
val quietTimeChecker = QuietTimeChecker(clock, quietTimeStarts, quietTimeEnds)
|
||||
return quietTimeChecker.isQuietTime
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
@JvmStatic
|
||||
fun isSortAscending(sortType: SortType): Boolean {
|
||||
if (sortAscending[sortType] == null) {
|
||||
sortAscending[sortType] = sortType.isDefaultAscending
|
||||
}
|
||||
return sortAscending[sortType]!!
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
@JvmStatic
|
||||
fun setSortAscending(sortType: SortType, sortAscending: Boolean) {
|
||||
K9.sortAscending[sortType] = sortAscending
|
||||
}
|
||||
|
||||
fun init(context: Context) {
|
||||
K9MailLib.setDebugStatus(object : K9MailLib.DebugStatus {
|
||||
override fun enabled(): Boolean = isDebugLoggingEnabled
|
||||
|
||||
override fun debugSensitive(): Boolean = isSensitiveDebugLoggingEnabled
|
||||
})
|
||||
com.fsck.k9.logging.Timber.logger = TimberLogger()
|
||||
|
||||
checkCachedDatabaseVersion(context)
|
||||
|
||||
loadPrefs(generalSettingsManager.storage)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun loadPrefs(storage: Storage) {
|
||||
isDebugLoggingEnabled = storage.getBoolean("enableDebugLogging", DEVELOPER_MODE)
|
||||
isSensitiveDebugLoggingEnabled = storage.getBoolean("enableSensitiveLogging", false)
|
||||
isShowAnimations = storage.getBoolean("animations", true)
|
||||
isUseVolumeKeysForNavigation = storage.getBoolean("useVolumeKeysForNavigation", false)
|
||||
isShowUnifiedInbox = storage.getBoolean("showUnifiedInbox", true)
|
||||
isShowStarredCount = storage.getBoolean("showStarredCount", false)
|
||||
isMessageListSenderAboveSubject = storage.getBoolean("messageListSenderAboveSubject", false)
|
||||
isShowMessageListStars = storage.getBoolean("messageListStars", true)
|
||||
messageListPreviewLines = storage.getInt("messageListPreviewLines", 2)
|
||||
|
||||
isAutoFitWidth = storage.getBoolean("autofitWidth", true)
|
||||
|
||||
isQuietTimeEnabled = storage.getBoolean("quietTimeEnabled", false)
|
||||
isNotificationDuringQuietTimeEnabled = storage.getBoolean("notificationDuringQuietTimeEnabled", true)
|
||||
quietTimeStarts = storage.getString("quietTimeStarts", "21:00")
|
||||
quietTimeEnds = storage.getString("quietTimeEnds", "7:00")
|
||||
|
||||
messageListDensity = storage.getEnum("messageListDensity", UiDensity.Default)
|
||||
isShowCorrespondentNames = storage.getBoolean("showCorrespondentNames", true)
|
||||
isShowContactName = storage.getBoolean("showContactName", false)
|
||||
isShowContactPicture = storage.getBoolean("showContactPicture", true)
|
||||
isChangeContactNameColor = storage.getBoolean("changeRegisteredNameColor", false)
|
||||
contactNameColor = storage.getInt("registeredNameColor", 0xFF1093F5.toInt())
|
||||
isUseMessageViewFixedWidthFont = storage.getBoolean("messageViewFixedWidthFont", false)
|
||||
isMessageViewReturnToList = storage.getBoolean("messageViewReturnToList", false)
|
||||
isMessageViewShowNext = storage.getBoolean("messageViewShowNext", false)
|
||||
isHideUserAgent = storage.getBoolean("hideUserAgent", false)
|
||||
isHideTimeZone = storage.getBoolean("hideTimeZone", false)
|
||||
|
||||
isConfirmDelete = storage.getBoolean("confirmDelete", false)
|
||||
isConfirmDiscardMessage = storage.getBoolean("confirmDiscardMessage", true)
|
||||
isConfirmDeleteStarred = storage.getBoolean("confirmDeleteStarred", false)
|
||||
isConfirmSpam = storage.getBoolean("confirmSpam", false)
|
||||
isConfirmDeleteFromNotification = storage.getBoolean("confirmDeleteFromNotification", true)
|
||||
isConfirmMarkAllRead = storage.getBoolean("confirmMarkAllRead", true)
|
||||
|
||||
sortType = storage.getEnum("sortTypeEnum", Account.DEFAULT_SORT_TYPE)
|
||||
|
||||
val sortAscendingSetting = storage.getBoolean("sortAscending", Account.DEFAULT_SORT_ASCENDING)
|
||||
sortAscending[sortType] = sortAscendingSetting
|
||||
|
||||
notificationQuickDeleteBehaviour = storage.getEnum("notificationQuickDelete", NotificationQuickDelete.ALWAYS)
|
||||
|
||||
lockScreenNotificationVisibility = storage.getEnum(
|
||||
"lockScreenNotificationVisibility",
|
||||
LockScreenNotificationVisibility.MESSAGE_COUNT
|
||||
)
|
||||
|
||||
splitViewMode = storage.getEnum("splitViewMode", SplitViewMode.NEVER)
|
||||
|
||||
isUseBackgroundAsUnreadIndicator = storage.getBoolean("useBackgroundAsUnreadIndicator", false)
|
||||
isShowComposeButtonOnMessageList = storage.getBoolean("showComposeButtonOnMessageList", true)
|
||||
isThreadedViewEnabled = storage.getBoolean("threadedView", true)
|
||||
fontSizes.load(storage)
|
||||
|
||||
backgroundOps = storage.getEnum("backgroundOperations", BACKGROUND_OPS.ALWAYS)
|
||||
|
||||
isColorizeMissingContactPictures = storage.getBoolean("colorizeMissingContactPictures", true)
|
||||
|
||||
isMessageViewArchiveActionVisible = storage.getBoolean("messageViewArchiveActionVisible", false)
|
||||
isMessageViewDeleteActionVisible = storage.getBoolean("messageViewDeleteActionVisible", true)
|
||||
isMessageViewMoveActionVisible = storage.getBoolean("messageViewMoveActionVisible", false)
|
||||
isMessageViewCopyActionVisible = storage.getBoolean("messageViewCopyActionVisible", false)
|
||||
isMessageViewSpamActionVisible = storage.getBoolean("messageViewSpamActionVisible", false)
|
||||
|
||||
pgpInlineDialogCounter = storage.getInt("pgpInlineDialogCounter", 0)
|
||||
pgpSignOnlyDialogCounter = storage.getInt("pgpSignOnlyDialogCounter", 0)
|
||||
|
||||
k9Language = storage.getString("language", "")
|
||||
|
||||
swipeRightAction = storage.getEnum("swipeRightAction", SwipeAction.ToggleSelection)
|
||||
swipeLeftAction = storage.getEnum("swipeLeftAction", SwipeAction.ToggleRead)
|
||||
}
|
||||
|
||||
internal fun save(editor: StorageEditor) {
|
||||
editor.putBoolean("enableDebugLogging", isDebugLoggingEnabled)
|
||||
editor.putBoolean("enableSensitiveLogging", isSensitiveDebugLoggingEnabled)
|
||||
editor.putEnum("backgroundOperations", backgroundOps)
|
||||
editor.putBoolean("animations", isShowAnimations)
|
||||
editor.putBoolean("useVolumeKeysForNavigation", isUseVolumeKeysForNavigation)
|
||||
editor.putBoolean("autofitWidth", isAutoFitWidth)
|
||||
editor.putBoolean("quietTimeEnabled", isQuietTimeEnabled)
|
||||
editor.putBoolean("notificationDuringQuietTimeEnabled", isNotificationDuringQuietTimeEnabled)
|
||||
editor.putString("quietTimeStarts", quietTimeStarts)
|
||||
editor.putString("quietTimeEnds", quietTimeEnds)
|
||||
|
||||
editor.putEnum("messageListDensity", messageListDensity)
|
||||
editor.putBoolean("messageListSenderAboveSubject", isMessageListSenderAboveSubject)
|
||||
editor.putBoolean("showUnifiedInbox", isShowUnifiedInbox)
|
||||
editor.putBoolean("showStarredCount", isShowStarredCount)
|
||||
editor.putBoolean("messageListStars", isShowMessageListStars)
|
||||
editor.putInt("messageListPreviewLines", messageListPreviewLines)
|
||||
editor.putBoolean("showCorrespondentNames", isShowCorrespondentNames)
|
||||
editor.putBoolean("showContactName", isShowContactName)
|
||||
editor.putBoolean("showContactPicture", isShowContactPicture)
|
||||
editor.putBoolean("changeRegisteredNameColor", isChangeContactNameColor)
|
||||
editor.putInt("registeredNameColor", contactNameColor)
|
||||
editor.putBoolean("messageViewFixedWidthFont", isUseMessageViewFixedWidthFont)
|
||||
editor.putBoolean("messageViewReturnToList", isMessageViewReturnToList)
|
||||
editor.putBoolean("messageViewShowNext", isMessageViewShowNext)
|
||||
editor.putBoolean("hideUserAgent", isHideUserAgent)
|
||||
editor.putBoolean("hideTimeZone", isHideTimeZone)
|
||||
|
||||
editor.putString("language", k9Language)
|
||||
|
||||
editor.putBoolean("confirmDelete", isConfirmDelete)
|
||||
editor.putBoolean("confirmDiscardMessage", isConfirmDiscardMessage)
|
||||
editor.putBoolean("confirmDeleteStarred", isConfirmDeleteStarred)
|
||||
editor.putBoolean("confirmSpam", isConfirmSpam)
|
||||
editor.putBoolean("confirmDeleteFromNotification", isConfirmDeleteFromNotification)
|
||||
editor.putBoolean("confirmMarkAllRead", isConfirmMarkAllRead)
|
||||
|
||||
editor.putEnum("sortTypeEnum", sortType)
|
||||
editor.putBoolean("sortAscending", sortAscending[sortType] ?: false)
|
||||
|
||||
editor.putString("notificationQuickDelete", notificationQuickDeleteBehaviour.toString())
|
||||
editor.putString("lockScreenNotificationVisibility", lockScreenNotificationVisibility.toString())
|
||||
|
||||
editor.putBoolean("useBackgroundAsUnreadIndicator", isUseBackgroundAsUnreadIndicator)
|
||||
editor.putBoolean("showComposeButtonOnMessageList", isShowComposeButtonOnMessageList)
|
||||
editor.putBoolean("threadedView", isThreadedViewEnabled)
|
||||
editor.putEnum("splitViewMode", splitViewMode)
|
||||
editor.putBoolean("colorizeMissingContactPictures", isColorizeMissingContactPictures)
|
||||
|
||||
editor.putBoolean("messageViewArchiveActionVisible", isMessageViewArchiveActionVisible)
|
||||
editor.putBoolean("messageViewDeleteActionVisible", isMessageViewDeleteActionVisible)
|
||||
editor.putBoolean("messageViewMoveActionVisible", isMessageViewMoveActionVisible)
|
||||
editor.putBoolean("messageViewCopyActionVisible", isMessageViewCopyActionVisible)
|
||||
editor.putBoolean("messageViewSpamActionVisible", isMessageViewSpamActionVisible)
|
||||
|
||||
editor.putInt("pgpInlineDialogCounter", pgpInlineDialogCounter)
|
||||
editor.putInt("pgpSignOnlyDialogCounter", pgpSignOnlyDialogCounter)
|
||||
|
||||
editor.putEnum("swipeRightAction", swipeRightAction)
|
||||
editor.putEnum("swipeLeftAction", swipeLeftAction)
|
||||
|
||||
fontSizes.save(editor)
|
||||
}
|
||||
|
||||
private fun updateLoggingStatus() {
|
||||
Timber.uprootAll()
|
||||
if (isDebugLoggingEnabled) {
|
||||
Timber.plant(DebugTree())
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun saveSettingsAsync() {
|
||||
generalSettingsManager.saveSettingsAsync()
|
||||
}
|
||||
|
||||
private inline fun <reified T : Enum<T>> Storage.getEnum(key: String, defaultValue: T): T {
|
||||
return try {
|
||||
val value = getString(key, null)
|
||||
if (value != null) {
|
||||
enumValueOf(value)
|
||||
} else {
|
||||
defaultValue
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Timber.e("Couldn't read setting '%s'. Using default value instead.", key)
|
||||
defaultValue
|
||||
}
|
||||
}
|
||||
|
||||
private fun <T : Enum<T>> StorageEditor.putEnum(key: String, value: T) {
|
||||
putString(key, value.name)
|
||||
}
|
||||
|
||||
const val LOCAL_UID_PREFIX = "K9LOCAL:"
|
||||
|
||||
const val IDENTITY_HEADER = K9MailLib.IDENTITY_HEADER
|
||||
|
||||
/**
|
||||
* Specifies how many messages will be shown in a folder by default. This number is set
|
||||
* on each new folder and can be incremented with "Load more messages..." by the
|
||||
* VISIBLE_LIMIT_INCREMENT
|
||||
*/
|
||||
const val DEFAULT_VISIBLE_LIMIT = 25
|
||||
|
||||
/**
|
||||
* The maximum size of an attachment we're willing to download (either View or Save)
|
||||
* Attachments that are base64 encoded (most) will be about 1.375x their actual size
|
||||
* so we should probably factor that in. A 5MB attachment will generally be around
|
||||
* 6.8MB downloaded but only 5MB saved.
|
||||
*/
|
||||
const val MAX_ATTACHMENT_DOWNLOAD_SIZE = 128 * 1024 * 1024
|
||||
|
||||
/**
|
||||
* How many times should K-9 try to deliver a message before giving up until the app is killed and restarted
|
||||
*/
|
||||
const val MAX_SEND_ATTEMPTS = 5
|
||||
|
||||
const val MANUAL_WAKE_LOCK_TIMEOUT = 120000
|
||||
const val PUSH_WAKE_LOCK_TIMEOUT = K9MailLib.PUSH_WAKE_LOCK_TIMEOUT
|
||||
const val MAIL_SERVICE_WAKE_LOCK_TIMEOUT = 60000
|
||||
const val BOOT_RECEIVER_WAKE_LOCK_TIMEOUT = 60000
|
||||
|
||||
enum class BACKGROUND_OPS {
|
||||
ALWAYS, NEVER, WHEN_CHECKED_AUTO_SYNC
|
||||
}
|
||||
|
||||
/**
|
||||
* Controls behaviour of delete button in notifications.
|
||||
*/
|
||||
enum class NotificationQuickDelete {
|
||||
ALWAYS,
|
||||
FOR_SINGLE_MSG,
|
||||
NEVER
|
||||
}
|
||||
|
||||
enum class LockScreenNotificationVisibility {
|
||||
EVERYTHING,
|
||||
SENDERS,
|
||||
MESSAGE_COUNT,
|
||||
APP_NAME,
|
||||
NOTHING
|
||||
}
|
||||
|
||||
/**
|
||||
* Controls when to use the message list split view.
|
||||
*/
|
||||
enum class SplitViewMode {
|
||||
ALWAYS,
|
||||
NEVER,
|
||||
WHEN_IN_LANDSCAPE
|
||||
}
|
||||
}
|
||||
40
app/core/src/main/java/com/fsck/k9/KoinModule.kt
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
package com.fsck.k9
|
||||
|
||||
import android.content.Context
|
||||
import app.k9mail.core.android.common.coreCommonAndroidModule
|
||||
import com.fsck.k9.helper.Contacts
|
||||
import com.fsck.k9.helper.DefaultTrustedSocketFactory
|
||||
import com.fsck.k9.mail.ssl.LocalKeyStore
|
||||
import com.fsck.k9.mail.ssl.TrustManagerFactory
|
||||
import com.fsck.k9.mail.ssl.TrustedSocketFactory
|
||||
import com.fsck.k9.mailstore.LocalStoreProvider
|
||||
import com.fsck.k9.setup.ServerNameSuggester
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.datetime.Clock
|
||||
import org.koin.core.qualifier.named
|
||||
import org.koin.dsl.module
|
||||
|
||||
val mainModule = module {
|
||||
includes(coreCommonAndroidModule)
|
||||
single<CoroutineScope>(named("AppCoroutineScope")) { GlobalScope }
|
||||
single {
|
||||
Preferences(
|
||||
storagePersister = get(),
|
||||
localStoreProvider = get(),
|
||||
accountPreferenceSerializer = get()
|
||||
)
|
||||
}
|
||||
single { get<Context>().resources }
|
||||
single { get<Context>().contentResolver }
|
||||
single { LocalStoreProvider() }
|
||||
single { Contacts() }
|
||||
single { LocalKeyStore(directoryProvider = get()) }
|
||||
single { TrustManagerFactory.createInstance(get()) }
|
||||
single { LocalKeyStoreManager(get()) }
|
||||
single<TrustedSocketFactory> { DefaultTrustedSocketFactory(get(), get()) }
|
||||
single<Clock> { Clock.System }
|
||||
factory { ServerNameSuggester() }
|
||||
factory { EmailAddressValidator() }
|
||||
factory { ServerSettingsSerializer() }
|
||||
}
|
||||
59
app/core/src/main/java/com/fsck/k9/LocalKeyStoreManager.kt
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
package com.fsck.k9
|
||||
|
||||
import com.fsck.k9.mail.MailServerDirection
|
||||
import com.fsck.k9.mail.ssl.LocalKeyStore
|
||||
import java.security.cert.CertificateException
|
||||
import java.security.cert.X509Certificate
|
||||
|
||||
class LocalKeyStoreManager(
|
||||
private val localKeyStore: LocalKeyStore
|
||||
) {
|
||||
/**
|
||||
* Add a new certificate for the incoming or outgoing server to the local key store.
|
||||
*/
|
||||
@Throws(CertificateException::class)
|
||||
fun addCertificate(account: Account, direction: MailServerDirection, certificate: X509Certificate) {
|
||||
val serverSettings = if (direction === MailServerDirection.INCOMING) {
|
||||
account.incomingServerSettings
|
||||
} else {
|
||||
account.outgoingServerSettings
|
||||
}
|
||||
localKeyStore.addCertificate(serverSettings.host!!, serverSettings.port, certificate)
|
||||
}
|
||||
|
||||
/**
|
||||
* Examine the existing settings for an account. If the old host/port is different from the
|
||||
* new host/port, then try and delete any (possibly non-existent) certificate stored for the
|
||||
* old host/port.
|
||||
*/
|
||||
fun deleteCertificate(account: Account, newHost: String, newPort: Int, direction: MailServerDirection) {
|
||||
val serverSettings = if (direction === MailServerDirection.INCOMING) {
|
||||
account.incomingServerSettings
|
||||
} else {
|
||||
account.outgoingServerSettings
|
||||
}
|
||||
val oldHost = serverSettings.host!!
|
||||
val oldPort = serverSettings.port
|
||||
if (oldPort == -1) {
|
||||
// This occurs when a new account is created
|
||||
return
|
||||
}
|
||||
if (newHost != oldHost || newPort != oldPort) {
|
||||
localKeyStore.deleteCertificate(oldHost, oldPort)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Examine the settings for the account and attempt to delete (possibly non-existent)
|
||||
* certificates for the incoming and outgoing servers.
|
||||
*/
|
||||
fun deleteCertificates(account: Account) {
|
||||
account.incomingServerSettings.let { serverSettings ->
|
||||
localKeyStore.deleteCertificate(serverSettings.host!!, serverSettings.port)
|
||||
}
|
||||
|
||||
account.outgoingServerSettings.let { serverSettings ->
|
||||
localKeyStore.deleteCertificate(serverSettings.host!!, serverSettings.port)
|
||||
}
|
||||
}
|
||||
}
|
||||
33
app/core/src/main/java/com/fsck/k9/NotificationLight.kt
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
package com.fsck.k9
|
||||
|
||||
import android.app.Notification
|
||||
|
||||
enum class NotificationLight {
|
||||
Disabled,
|
||||
AccountColor,
|
||||
SystemDefaultColor,
|
||||
White,
|
||||
Red,
|
||||
Green,
|
||||
Blue,
|
||||
Yellow,
|
||||
Cyan,
|
||||
Magenta;
|
||||
|
||||
fun toColor(account: Account): Int? {
|
||||
return when (this) {
|
||||
Disabled -> null
|
||||
AccountColor -> account.chipColor.toArgb()
|
||||
SystemDefaultColor -> Notification.COLOR_DEFAULT
|
||||
White -> 0xFFFFFF.toArgb()
|
||||
Red -> 0xFF0000.toArgb()
|
||||
Green -> 0x00FF00.toArgb()
|
||||
Blue -> 0x0000FF.toArgb()
|
||||
Yellow -> 0xFFFF00.toArgb()
|
||||
Cyan -> 0x00FFFF.toArgb()
|
||||
Magenta -> 0xFF00FF.toArgb()
|
||||
}
|
||||
}
|
||||
|
||||
private fun Int.toArgb() = this or 0xFF000000L.toInt()
|
||||
}
|
||||
11
app/core/src/main/java/com/fsck/k9/NotificationSettings.kt
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
package com.fsck.k9
|
||||
|
||||
/**
|
||||
* Describes how a notification should behave.
|
||||
*/
|
||||
data class NotificationSettings(
|
||||
val isRingEnabled: Boolean = false,
|
||||
val ringtone: String? = null,
|
||||
val light: NotificationLight = NotificationLight.Disabled,
|
||||
val vibration: NotificationVibration = NotificationVibration.DEFAULT
|
||||
)
|
||||
62
app/core/src/main/java/com/fsck/k9/NotificationVibration.kt
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
package com.fsck.k9
|
||||
|
||||
data class NotificationVibration(
|
||||
val isEnabled: Boolean,
|
||||
val pattern: VibratePattern,
|
||||
val repeatCount: Int
|
||||
) {
|
||||
val systemPattern: LongArray
|
||||
get() = getSystemPattern(pattern, repeatCount)
|
||||
|
||||
companion object {
|
||||
val DEFAULT = NotificationVibration(isEnabled = false, pattern = VibratePattern.Default, repeatCount = 5)
|
||||
|
||||
fun getSystemPattern(vibratePattern: VibratePattern, repeatCount: Int): LongArray {
|
||||
val selectedPattern = vibratePattern.vibrationPattern
|
||||
val repeatedPattern = LongArray(selectedPattern.size * repeatCount)
|
||||
for (n in 0 until repeatCount) {
|
||||
System.arraycopy(selectedPattern, 0, repeatedPattern, n * selectedPattern.size, selectedPattern.size)
|
||||
}
|
||||
|
||||
// Do not wait before starting the vibration pattern.
|
||||
repeatedPattern[0] = 0
|
||||
|
||||
return repeatedPattern
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum class VibratePattern(
|
||||
/**
|
||||
* These are "off, on" patterns, specified in milliseconds.
|
||||
*/
|
||||
val vibrationPattern: LongArray
|
||||
) {
|
||||
Default(vibrationPattern = longArrayOf(300, 200)),
|
||||
Pattern1(vibrationPattern = longArrayOf(100, 200)),
|
||||
Pattern2(vibrationPattern = longArrayOf(100, 500)),
|
||||
Pattern3(vibrationPattern = longArrayOf(200, 200)),
|
||||
Pattern4(vibrationPattern = longArrayOf(200, 500)),
|
||||
Pattern5(vibrationPattern = longArrayOf(500, 500));
|
||||
|
||||
fun serialize(): Int = when (this) {
|
||||
Default -> 0
|
||||
Pattern1 -> 1
|
||||
Pattern2 -> 2
|
||||
Pattern3 -> 3
|
||||
Pattern4 -> 4
|
||||
Pattern5 -> 5
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun deserialize(value: Int): VibratePattern = when (value) {
|
||||
0 -> Default
|
||||
1 -> Pattern1
|
||||
2 -> Pattern2
|
||||
3 -> Pattern3
|
||||
4 -> Pattern4
|
||||
5 -> Pattern5
|
||||
else -> error("Unknown VibratePattern value: $value")
|
||||
}
|
||||
}
|
||||
}
|
||||
300
app/core/src/main/java/com/fsck/k9/Preferences.kt
Normal file
|
|
@ -0,0 +1,300 @@
|
|||
package com.fsck.k9
|
||||
|
||||
import androidx.annotation.GuardedBy
|
||||
import androidx.annotation.RestrictTo
|
||||
import com.fsck.k9.mail.MessagingException
|
||||
import com.fsck.k9.mailstore.LocalStoreProvider
|
||||
import com.fsck.k9.preferences.AccountManager
|
||||
import com.fsck.k9.preferences.Storage
|
||||
import com.fsck.k9.preferences.StorageEditor
|
||||
import com.fsck.k9.preferences.StoragePersister
|
||||
import java.util.HashMap
|
||||
import java.util.LinkedList
|
||||
import java.util.UUID
|
||||
import java.util.concurrent.CopyOnWriteArraySet
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.channels.awaitClose
|
||||
import kotlinx.coroutines.channels.trySendBlocking
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.buffer
|
||||
import kotlinx.coroutines.flow.callbackFlow
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import timber.log.Timber
|
||||
|
||||
class Preferences internal constructor(
|
||||
private val storagePersister: StoragePersister,
|
||||
private val localStoreProvider: LocalStoreProvider,
|
||||
private val accountPreferenceSerializer: AccountPreferenceSerializer,
|
||||
private val backgroundDispatcher: CoroutineDispatcher = Dispatchers.IO
|
||||
) : AccountManager {
|
||||
private val accountLock = Any()
|
||||
private val storageLock = Any()
|
||||
|
||||
@GuardedBy("accountLock")
|
||||
private var accountsMap: MutableMap<String, Account>? = null
|
||||
|
||||
@GuardedBy("accountLock")
|
||||
private var accountsInOrder = mutableListOf<Account>()
|
||||
|
||||
@GuardedBy("accountLock")
|
||||
private var newAccount: Account? = null
|
||||
private val accountsChangeListeners = CopyOnWriteArraySet<AccountsChangeListener>()
|
||||
private val accountRemovedListeners = CopyOnWriteArraySet<AccountRemovedListener>()
|
||||
|
||||
@GuardedBy("storageLock")
|
||||
private var currentStorage: Storage? = null
|
||||
|
||||
val storage: Storage
|
||||
get() = synchronized(storageLock) {
|
||||
currentStorage ?: storagePersister.loadValues().also { newStorage ->
|
||||
currentStorage = newStorage
|
||||
}
|
||||
}
|
||||
|
||||
fun createStorageEditor(): StorageEditor {
|
||||
return storagePersister.createStorageEditor { updater ->
|
||||
synchronized(storageLock) {
|
||||
currentStorage = updater(storage)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@RestrictTo(RestrictTo.Scope.TESTS)
|
||||
fun clearAccounts() {
|
||||
synchronized(accountLock) {
|
||||
accountsMap = HashMap()
|
||||
accountsInOrder = LinkedList()
|
||||
}
|
||||
}
|
||||
|
||||
fun loadAccounts() {
|
||||
synchronized(accountLock) {
|
||||
val accounts = mutableMapOf<String, Account>()
|
||||
val accountsInOrder = mutableListOf<Account>()
|
||||
|
||||
val accountUuids = storage.getString("accountUuids", null)
|
||||
if (!accountUuids.isNullOrEmpty()) {
|
||||
accountUuids.split(",").forEach { uuid ->
|
||||
val newAccount = Account(uuid)
|
||||
accountPreferenceSerializer.loadAccount(newAccount, storage)
|
||||
|
||||
accounts[uuid] = newAccount
|
||||
accountsInOrder.add(newAccount)
|
||||
}
|
||||
}
|
||||
|
||||
newAccount?.takeIf { it.accountNumber != -1 }?.let { newAccount ->
|
||||
accounts[newAccount.uuid] = newAccount
|
||||
if (newAccount !in accountsInOrder) {
|
||||
accountsInOrder.add(newAccount)
|
||||
}
|
||||
this.newAccount = null
|
||||
}
|
||||
|
||||
this.accountsMap = accounts
|
||||
this.accountsInOrder = accountsInOrder
|
||||
}
|
||||
}
|
||||
|
||||
val accounts: List<Account>
|
||||
get() {
|
||||
synchronized(accountLock) {
|
||||
if (accountsMap == null) {
|
||||
loadAccounts()
|
||||
}
|
||||
|
||||
return accountsInOrder.toList()
|
||||
}
|
||||
}
|
||||
|
||||
private val completeAccounts: List<Account>
|
||||
get() = accounts.filter { it.isFinishedSetup }
|
||||
|
||||
override fun getAccount(accountUuid: String): Account? {
|
||||
synchronized(accountLock) {
|
||||
if (accountsMap == null) {
|
||||
loadAccounts()
|
||||
}
|
||||
|
||||
return accountsMap!![accountUuid]
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
override fun getAccountFlow(accountUuid: String): Flow<Account> {
|
||||
return callbackFlow {
|
||||
val initialAccount = getAccount(accountUuid)
|
||||
if (initialAccount == null) {
|
||||
close()
|
||||
return@callbackFlow
|
||||
}
|
||||
|
||||
send(initialAccount)
|
||||
|
||||
val listener = AccountsChangeListener {
|
||||
val account = getAccount(accountUuid)
|
||||
if (account != null) {
|
||||
trySendBlocking(account)
|
||||
} else {
|
||||
close()
|
||||
}
|
||||
}
|
||||
addOnAccountsChangeListener(listener)
|
||||
|
||||
awaitClose {
|
||||
removeOnAccountsChangeListener(listener)
|
||||
}
|
||||
}.buffer(capacity = Channel.CONFLATED)
|
||||
.flowOn(backgroundDispatcher)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
override fun getAccountsFlow(): Flow<List<Account>> {
|
||||
return callbackFlow {
|
||||
send(completeAccounts)
|
||||
|
||||
val listener = AccountsChangeListener {
|
||||
trySendBlocking(completeAccounts)
|
||||
}
|
||||
addOnAccountsChangeListener(listener)
|
||||
|
||||
awaitClose {
|
||||
removeOnAccountsChangeListener(listener)
|
||||
}
|
||||
}.buffer(capacity = Channel.CONFLATED)
|
||||
.flowOn(backgroundDispatcher)
|
||||
}
|
||||
|
||||
fun newAccount(): Account {
|
||||
val accountUuid = UUID.randomUUID().toString()
|
||||
val account = Account(accountUuid)
|
||||
accountPreferenceSerializer.loadDefaults(account)
|
||||
|
||||
synchronized(accountLock) {
|
||||
newAccount = account
|
||||
accountsMap!![account.uuid] = account
|
||||
accountsInOrder.add(account)
|
||||
}
|
||||
|
||||
return account
|
||||
}
|
||||
|
||||
fun deleteAccount(account: Account) {
|
||||
synchronized(accountLock) {
|
||||
accountsMap?.remove(account.uuid)
|
||||
accountsInOrder.remove(account)
|
||||
|
||||
val storageEditor = createStorageEditor()
|
||||
accountPreferenceSerializer.delete(storageEditor, storage, account)
|
||||
storageEditor.commit()
|
||||
|
||||
if (account === newAccount) {
|
||||
newAccount = null
|
||||
}
|
||||
}
|
||||
|
||||
notifyAccountRemovedListeners(account)
|
||||
notifyAccountsChangeListeners()
|
||||
}
|
||||
|
||||
val defaultAccount: Account?
|
||||
get() = accounts.firstOrNull()
|
||||
|
||||
override fun saveAccount(account: Account) {
|
||||
ensureAssignedAccountNumber(account)
|
||||
processChangedValues(account)
|
||||
|
||||
synchronized(accountLock) {
|
||||
val editor = createStorageEditor()
|
||||
accountPreferenceSerializer.save(editor, storage, account)
|
||||
editor.commit()
|
||||
}
|
||||
|
||||
notifyAccountsChangeListeners()
|
||||
}
|
||||
|
||||
private fun ensureAssignedAccountNumber(account: Account) {
|
||||
if (account.accountNumber != Account.UNASSIGNED_ACCOUNT_NUMBER) return
|
||||
|
||||
account.accountNumber = generateAccountNumber()
|
||||
}
|
||||
|
||||
private fun processChangedValues(account: Account) {
|
||||
if (account.isChangedVisibleLimits) {
|
||||
try {
|
||||
localStoreProvider.getInstance(account).resetVisibleLimits(account.displayCount)
|
||||
} catch (e: MessagingException) {
|
||||
Timber.e(e, "Failed to load LocalStore!")
|
||||
}
|
||||
}
|
||||
account.resetChangeMarkers()
|
||||
}
|
||||
|
||||
fun generateAccountNumber(): Int {
|
||||
val accountNumbers = accounts.map { it.accountNumber }
|
||||
return findNewAccountNumber(accountNumbers)
|
||||
}
|
||||
|
||||
private fun findNewAccountNumber(accountNumbers: List<Int>): Int {
|
||||
var newAccountNumber = -1
|
||||
for (accountNumber in accountNumbers.sorted()) {
|
||||
if (accountNumber > newAccountNumber + 1) {
|
||||
break
|
||||
}
|
||||
newAccountNumber = accountNumber
|
||||
}
|
||||
newAccountNumber++
|
||||
|
||||
return newAccountNumber
|
||||
}
|
||||
|
||||
override fun moveAccount(account: Account, newPosition: Int) {
|
||||
synchronized(accountLock) {
|
||||
val storageEditor = createStorageEditor()
|
||||
accountPreferenceSerializer.move(storageEditor, account, storage, newPosition)
|
||||
storageEditor.commit()
|
||||
|
||||
loadAccounts()
|
||||
}
|
||||
|
||||
notifyAccountsChangeListeners()
|
||||
}
|
||||
|
||||
private fun notifyAccountsChangeListeners() {
|
||||
for (listener in accountsChangeListeners) {
|
||||
listener.onAccountsChanged()
|
||||
}
|
||||
}
|
||||
|
||||
override fun addOnAccountsChangeListener(accountsChangeListener: AccountsChangeListener) {
|
||||
accountsChangeListeners.add(accountsChangeListener)
|
||||
}
|
||||
|
||||
override fun removeOnAccountsChangeListener(accountsChangeListener: AccountsChangeListener) {
|
||||
accountsChangeListeners.remove(accountsChangeListener)
|
||||
}
|
||||
|
||||
private fun notifyAccountRemovedListeners(account: Account) {
|
||||
for (listener in accountRemovedListeners) {
|
||||
listener.onAccountRemoved(account)
|
||||
}
|
||||
}
|
||||
|
||||
override fun addAccountRemovedListener(listener: AccountRemovedListener) {
|
||||
accountRemovedListeners.add(listener)
|
||||
}
|
||||
|
||||
fun removeAccountRemovedListener(listener: AccountRemovedListener) {
|
||||
accountRemovedListeners.remove(listener)
|
||||
}
|
||||
|
||||
companion object {
|
||||
@JvmStatic
|
||||
fun getPreferences(): Preferences {
|
||||
return DI.get()
|
||||
}
|
||||
}
|
||||
}
|
||||
46
app/core/src/main/java/com/fsck/k9/QuietTimeChecker.java
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
package com.fsck.k9;
|
||||
|
||||
|
||||
import java.util.Calendar;
|
||||
|
||||
import kotlinx.datetime.Clock;
|
||||
|
||||
|
||||
class QuietTimeChecker {
|
||||
private final Clock clock;
|
||||
private final int quietTimeStart;
|
||||
private final int quietTimeEnd;
|
||||
|
||||
|
||||
QuietTimeChecker(Clock clock, String quietTimeStart, String quietTimeEnd) {
|
||||
this.clock = clock;
|
||||
this.quietTimeStart = parseTime(quietTimeStart);
|
||||
this.quietTimeEnd = parseTime(quietTimeEnd);
|
||||
}
|
||||
|
||||
private static int parseTime(String time) {
|
||||
String[] parts = time.split(":");
|
||||
int hour = Integer.parseInt(parts[0]);
|
||||
int minute = Integer.parseInt(parts[1]);
|
||||
|
||||
return hour * 60 + minute;
|
||||
}
|
||||
|
||||
public boolean isQuietTime() {
|
||||
// If start and end times are the same, we're never quiet
|
||||
if (quietTimeStart == quietTimeEnd) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Calendar calendar = Calendar.getInstance();
|
||||
calendar.setTimeInMillis(clock.now().toEpochMilliseconds());
|
||||
|
||||
int minutesSinceMidnight = (calendar.get(Calendar.HOUR_OF_DAY) * 60) + calendar.get(Calendar.MINUTE);
|
||||
|
||||
if (quietTimeStart > quietTimeEnd) {
|
||||
return minutesSinceMidnight >= quietTimeStart || minutesSinceMidnight <= quietTimeEnd;
|
||||
} else {
|
||||
return minutesSinceMidnight >= quietTimeStart && minutesSinceMidnight <= quietTimeEnd;
|
||||
}
|
||||
}
|
||||
}
|
||||
122
app/core/src/main/java/com/fsck/k9/ServerSettingsSerializer.kt
Normal file
|
|
@ -0,0 +1,122 @@
|
|||
package com.fsck.k9
|
||||
|
||||
import com.fsck.k9.mail.AuthType
|
||||
import com.fsck.k9.mail.ConnectionSecurity
|
||||
import com.fsck.k9.mail.ServerSettings
|
||||
import com.squareup.moshi.JsonAdapter
|
||||
import com.squareup.moshi.JsonReader
|
||||
import com.squareup.moshi.JsonReader.Token
|
||||
import com.squareup.moshi.JsonWriter
|
||||
|
||||
class ServerSettingsSerializer {
|
||||
private val adapter = ServerSettingsAdapter()
|
||||
|
||||
fun serialize(serverSettings: ServerSettings): String {
|
||||
return adapter.toJson(serverSettings)
|
||||
}
|
||||
|
||||
fun deserialize(json: String): ServerSettings {
|
||||
return adapter.fromJson(json)!!
|
||||
}
|
||||
}
|
||||
|
||||
private const val KEY_TYPE = "type"
|
||||
private const val KEY_HOST = "host"
|
||||
private const val KEY_PORT = "port"
|
||||
private const val KEY_CONNECTION_SECURITY = "connectionSecurity"
|
||||
private const val KEY_AUTHENTICATION_TYPE = "authenticationType"
|
||||
private const val KEY_USERNAME = "username"
|
||||
private const val KEY_PASSWORD = "password"
|
||||
private const val KEY_CLIENT_CERTIFICATE_ALIAS = "clientCertificateAlias"
|
||||
|
||||
private val JSON_KEYS = JsonReader.Options.of(
|
||||
KEY_TYPE,
|
||||
KEY_HOST,
|
||||
KEY_PORT,
|
||||
KEY_CONNECTION_SECURITY,
|
||||
KEY_AUTHENTICATION_TYPE,
|
||||
KEY_USERNAME,
|
||||
KEY_PASSWORD,
|
||||
KEY_CLIENT_CERTIFICATE_ALIAS
|
||||
)
|
||||
|
||||
private class ServerSettingsAdapter : JsonAdapter<ServerSettings>() {
|
||||
override fun fromJson(reader: JsonReader): ServerSettings {
|
||||
reader.beginObject()
|
||||
|
||||
var type: String? = null
|
||||
var host: String? = null
|
||||
var port: Int? = null
|
||||
var connectionSecurity: ConnectionSecurity? = null
|
||||
var authenticationType: AuthType? = null
|
||||
var username: String? = null
|
||||
var password: String? = null
|
||||
var clientCertificateAlias: String? = null
|
||||
val extra = mutableMapOf<String, String?>()
|
||||
|
||||
while (reader.hasNext()) {
|
||||
when (reader.selectName(JSON_KEYS)) {
|
||||
0 -> type = reader.nextString()
|
||||
1 -> host = reader.nextString()
|
||||
2 -> port = reader.nextInt()
|
||||
3 -> connectionSecurity = ConnectionSecurity.valueOf(reader.nextString())
|
||||
4 -> authenticationType = AuthType.valueOf(reader.nextString())
|
||||
5 -> username = reader.nextString()
|
||||
6 -> password = reader.nextStringOrNull()
|
||||
7 -> clientCertificateAlias = reader.nextStringOrNull()
|
||||
else -> {
|
||||
val key = reader.nextName()
|
||||
val value = reader.nextStringOrNull()
|
||||
extra[key] = value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
reader.endObject()
|
||||
|
||||
requireNotNull(type) { "'type' must not be missing" }
|
||||
requireNotNull(host) { "'host' must not be missing" }
|
||||
requireNotNull(port) { "'port' must not be missing" }
|
||||
requireNotNull(connectionSecurity) { "'connectionSecurity' must not be missing" }
|
||||
requireNotNull(authenticationType) { "'authenticationType' must not be missing" }
|
||||
requireNotNull(username) { "'username' must not be missing" }
|
||||
|
||||
return ServerSettings(
|
||||
type,
|
||||
host,
|
||||
port,
|
||||
connectionSecurity,
|
||||
authenticationType,
|
||||
username,
|
||||
password,
|
||||
clientCertificateAlias,
|
||||
extra
|
||||
)
|
||||
}
|
||||
|
||||
override fun toJson(writer: JsonWriter, serverSettings: ServerSettings?) {
|
||||
requireNotNull(serverSettings)
|
||||
|
||||
writer.beginObject()
|
||||
writer.serializeNulls = true
|
||||
|
||||
writer.name(KEY_TYPE).value(serverSettings.type)
|
||||
writer.name(KEY_HOST).value(serverSettings.host)
|
||||
writer.name(KEY_PORT).value(serverSettings.port)
|
||||
writer.name(KEY_CONNECTION_SECURITY).value(serverSettings.connectionSecurity.name)
|
||||
writer.name(KEY_AUTHENTICATION_TYPE).value(serverSettings.authenticationType.name)
|
||||
writer.name(KEY_USERNAME).value(serverSettings.username)
|
||||
writer.name(KEY_PASSWORD).value(serverSettings.password)
|
||||
writer.name(KEY_CLIENT_CERTIFICATE_ALIAS).value(serverSettings.clientCertificateAlias)
|
||||
|
||||
for ((key, value) in serverSettings.extra) {
|
||||
writer.name(key).value(value)
|
||||
}
|
||||
|
||||
writer.endObject()
|
||||
}
|
||||
|
||||
private fun JsonReader.nextStringOrNull(): String? {
|
||||
return if (peek() == Token.NULL) nextNull() else nextString()
|
||||
}
|
||||
}
|
||||
44
app/core/src/main/java/com/fsck/k9/StrictMode.kt
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
package com.fsck.k9
|
||||
|
||||
import android.os.Build
|
||||
import android.os.StrictMode
|
||||
import android.os.StrictMode.ThreadPolicy
|
||||
import android.os.StrictMode.VmPolicy
|
||||
|
||||
fun enableStrictMode() {
|
||||
StrictMode.setThreadPolicy(createThreadPolicy())
|
||||
StrictMode.setVmPolicy(createVmPolicy())
|
||||
}
|
||||
|
||||
private fun createThreadPolicy(): ThreadPolicy {
|
||||
return ThreadPolicy.Builder()
|
||||
.detectAll()
|
||||
.penaltyLog()
|
||||
.build()
|
||||
}
|
||||
|
||||
private fun createVmPolicy(): VmPolicy {
|
||||
return VmPolicy.Builder()
|
||||
.detectActivityLeaks()
|
||||
.detectLeakedClosableObjects()
|
||||
.detectLeakedRegistrationObjects()
|
||||
.detectFileUriExposure()
|
||||
.detectLeakedSqlLiteObjects()
|
||||
.apply {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
detectContentUriWithoutPermission()
|
||||
|
||||
// Disabled because we currently don't use tagged sockets; so this would generate a lot of noise
|
||||
// detectUntaggedSockets()
|
||||
}
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
detectCredentialProtectedWhileLocked()
|
||||
}
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
detectIncorrectContextUse()
|
||||
detectUnsafeIntentLaunch()
|
||||
}
|
||||
}
|
||||
.penaltyLog()
|
||||
.build()
|
||||
}
|
||||
12
app/core/src/main/java/com/fsck/k9/SwipeAction.kt
Normal file
|
|
@ -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)
|
||||
}
|
||||