Repo created

This commit is contained in:
Fr4nz D13trich 2025-11-22 13:56:56 +01:00
parent 75dc487a7a
commit 39c29d175b
6317 changed files with 388324 additions and 2 deletions

25
.editorconfig Normal file
View file

@ -0,0 +1,25 @@
root = true
[*]
charset = utf-8
indent_style = space
indent_size = 4
ij_continuation_indent_size = 4
tab_width = 4
insert_final_newline = true
trim_trailing_whitespace = true
end_of_line = lf
max_line_length = 120
[*.{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
tab_width = 2
ij_continuation_indent_size = 2
[*.md]
trim_trailing_whitespace = false

9
.gitattributes vendored Normal file
View file

@ -0,0 +1,9 @@
* text=auto
*.bat eol=crlf
*.eml eol=crlf
*.jar binary
app-k9mail/build.gradle.kts merge=merge_gradle
app-thunderbird/build.gradle.kts merge=merge_gradle
app-k9mail/src/main/res/raw/changelog_master.xml merge=ours

48
.gitignore vendored Normal file
View file

@ -0,0 +1,48 @@
# Gradle
.gradle/
local.properties
# Kotlin
.kotlin/
# mdBook
book/
# Generated folders
bin/
build/
gen/
out/
# Generated files
*.aab
*.apk
*.ap_
*.class
*.dex
# Keystore files
*.jks
*.keystore
# Signing files
.signing/
*.signing.properties
# IDEA/Android Studio ignores
*iml
.idea/*
# IDEA/Android Studio includes
!.idea/icon.png
!.idea/codeStyles/
!.idea/fileTemplates/
# Android Studio captures folder
captures/
# Mac thumbnail db
.DS_Store
# Screenshots
adb-screenshots/

447
.idea/codeStyles/Project.xml generated Normal file
View file

@ -0,0 +1,447 @@
<component name="ProjectCodeStyleConfiguration">
<code_scheme name="Project" version="173">
<option name="LINE_SEPARATOR" value="&#10;" />
<option name="RIGHT_MARGIN" value="120" />
<option name="SOFT_MARGINS" value="120" />
<AndroidXmlCodeStyleSettings>
<option name="LAYOUT_SETTINGS">
<value>
<option name="INSERT_LINE_BREAK_BEFORE_NAMESPACE_DECLARATION" value="true" />
<option name="INSERT_LINE_BREAK_AFTER_LAST_ATTRIBUTE" value="true" />
</value>
</option>
<option name="MANIFEST_SETTINGS">
<value>
<option name="INSERT_LINE_BREAK_BEFORE_NAMESPACE_DECLARATION" value="true" />
<option name="INSERT_LINE_BREAK_AFTER_LAST_ATTRIBUTE" value="true" />
</value>
</option>
<option name="OTHER_SETTINGS">
<value>
<option name="INSERT_LINE_BREAK_BEFORE_NAMESPACE_DECLARATION" value="true" />
<option name="INSERT_LINE_BREAK_AFTER_LAST_ATTRIBUTE" value="true" />
</value>
</option>
</AndroidXmlCodeStyleSettings>
<JavaCodeStyleSettings>
<option name="SPACE_AROUND_TYPE_BOUNDS_IN_TYPE_PARAMETERS" value="false" />
<option name="INSERT_INNER_CLASS_IMPORTS" value="true" />
<option name="IMPORT_LAYOUT_TABLE">
<value>
<package name="" withSubpackages="true" static="false" module="true" />
<package name="java" withSubpackages="true" static="false" />
<emptyLine />
<package name="android" withSubpackages="true" static="false" />
<emptyLine />
<package name="" withSubpackages="true" static="false" />
<emptyLine />
<package name="" withSubpackages="true" static="true" />
<emptyLine />
</value>
</option>
</JavaCodeStyleSettings>
<JetCodeStyleSettings>
<option name="PACKAGES_IMPORT_LAYOUT">
<value>
<package name="" alias="false" withSubpackages="true" />
<package name="" alias="true" withSubpackages="true" />
</value>
</option>
<option name="NAME_COUNT_TO_USE_STAR_IMPORT" value="99" />
<option name="NAME_COUNT_TO_USE_STAR_IMPORT_FOR_MEMBERS" value="99" />
<option name="ALLOW_TRAILING_COMMA" value="true" />
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
</JetCodeStyleSettings>
<codeStyleSettings language="JAVA">
<option name="KEEP_CONTROL_STATEMENT_IN_ONE_LINE" value="false" />
<option name="BLANK_LINES_AFTER_PACKAGE" value="2" />
<option name="BLANK_LINES_AFTER_IMPORTS" value="2" />
<option name="ALIGN_MULTILINE_PARAMETERS" value="false" />
<option name="SPACE_WITHIN_ARRAY_INITIALIZER_BRACES" value="true" />
<option name="SPACE_BEFORE_ARRAY_INITIALIZER_LBRACE" value="true" />
<option name="CALL_PARAMETERS_WRAP" value="1" />
<option name="METHOD_PARAMETERS_WRAP" value="1" />
<option name="RESOURCE_LIST_WRAP" value="1" />
<option name="EXTENDS_LIST_WRAP" value="1" />
<option name="THROWS_LIST_WRAP" value="1" />
<option name="EXTENDS_KEYWORD_WRAP" value="1" />
<option name="THROWS_KEYWORD_WRAP" value="1" />
<option name="METHOD_CALL_CHAIN_WRAP" value="1" />
<option name="BINARY_OPERATION_WRAP" value="1" />
<option name="TERNARY_OPERATION_WRAP" value="1" />
<option name="FOR_STATEMENT_WRAP" value="1" />
<option name="ARRAY_INITIALIZER_WRAP" value="1" />
<option name="ASSIGNMENT_WRAP" value="1" />
<option name="WRAP_COMMENTS" value="true" />
<option name="ASSERT_STATEMENT_WRAP" value="1" />
<option name="IF_BRACE_FORCE" value="3" />
<option name="DOWHILE_BRACE_FORCE" value="3" />
<option name="WHILE_BRACE_FORCE" value="3" />
<option name="FOR_BRACE_FORCE" value="3" />
<arrangement>
<rules>
<section>
<rule>
<match>
<AND>
<FIELD />
<FINAL />
<PUBLIC />
<STATIC />
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<FIELD />
<FINAL />
<PROTECTED />
<STATIC />
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<FIELD />
<FINAL />
<PACKAGE_PRIVATE />
<STATIC />
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<FIELD />
<FINAL />
<PRIVATE />
<STATIC />
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<FIELD />
<PUBLIC />
<STATIC />
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<FIELD />
<PROTECTED />
<STATIC />
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<FIELD />
<PACKAGE_PRIVATE />
<STATIC />
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<FIELD />
<PRIVATE />
<STATIC />
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<FIELD />
<FINAL />
<PUBLIC />
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<FIELD />
<FINAL />
<PROTECTED />
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<FIELD />
<FINAL />
<PACKAGE_PRIVATE />
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<FIELD />
<FINAL />
<PRIVATE />
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<FIELD />
<PUBLIC />
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<FIELD />
<PROTECTED />
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<FIELD />
<PACKAGE_PRIVATE />
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<FIELD />
<PRIVATE />
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<FIELD />
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<METHOD />
<STATIC />
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<CONSTRUCTOR />
</match>
</rule>
</section>
<section>
<rule>
<match>
<METHOD />
</match>
</rule>
</section>
<section>
<rule>
<match>
<ENUM />
</match>
</rule>
</section>
<section>
<rule>
<match>
<INTERFACE />
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<CLASS />
<STATIC />
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<CLASS />
</match>
</rule>
</section>
</rules>
</arrangement>
</codeStyleSettings>
<codeStyleSettings language="XML">
<indentOptions>
<option name="CONTINUATION_INDENT_SIZE" value="4" />
</indentOptions>
<arrangement>
<rules>
<section>
<rule>
<match>
<AND>
<NAME>xmlns:android</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>xmlns:.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:id</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:name</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>name</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>style</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
<order>ANDROID_ATTRIBUTE_ORDER</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>.*</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
</rules>
</arrangement>
</codeStyleSettings>
<codeStyleSettings language="kotlin">
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
<option name="KEEP_BLANK_LINES_IN_DECLARATIONS" value="1" />
<option name="KEEP_BLANK_LINES_IN_CODE" value="1" />
<option name="KEEP_BLANK_LINES_BEFORE_RBRACE" value="0" />
<option name="ALIGN_MULTILINE_PARAMETERS" value="false" />
<indentOptions>
<option name="CONTINUATION_INDENT_SIZE" value="4" />
</indentOptions>
</codeStyleSettings>
</code_scheme>
</component>

5
.idea/codeStyles/codeStyleConfig.xml generated Normal file
View file

@ -0,0 +1,5 @@
<component name="ProjectCodeStyleConfiguration">
<state>
<option name="USE_PER_PROJECT_SETTINGS" value="true" />
</state>
</component>

BIN
.idea/icon.png generated Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

1
.java-version Normal file
View file

@ -0,0 +1 @@
21

3
CODE_OF_CONDUCT.md Normal file
View file

@ -0,0 +1,3 @@
# Code of Conduct
This repository is governed by Mozilla's code of conduct and etiquette guidelines. For more details please see the [Mozilla Community Participation Guidelines](https://www.mozilla.org/about/governance/policies/participation/).

202
LICENSE Normal file
View file

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

3
NOTICE Normal file
View file

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

View file

@ -1,3 +1,89 @@
# k9-mail
# Thunderbird for Android
Android Mail Client
<a href="https://play.google.com/store/apps/details?id=net.thunderbird.android&referrer=utm_campaign%3Dandroid_metadata%26utm_medium%3Dweb%26utm_source%3Dgithub.com%26utm_content%3Dbadge" target="_blank"><img src="./docs/assets/get-it-on-play.png" alt="Get it on Google Play" height="28"></a>
<a href="https://f-droid.org/packages/net.thunderbird.android"><img src="./docs/assets/get-it-on-fdroid.png" alt="Get it on F-Droid" height="28"></a>
[![Latest release](https://img.shields.io/github/release/thunderbird/thunderbird-android.svg?style=for-the-badge&filter=THUNDERBIRD_*&logo=thunderbird)](https://github.com/thunderbird/thunderbird-android/releases/latest)
[![Latest beta release](https://img.shields.io/github/release/thunderbird/thunderbird-android.svg?include_prereleases&style=for-the-badge&label=beta&filter=THUNDERBIRD_*b*&logo=thunderbird)](https://github.com/thunderbird/thunderbird-android/releases)
Thunderbird for Android is a powerful, privacy-focused email app. Effortlessly manage multiple email accounts from one app, with a Unified Inbox option for maximum productivity. Built on open-source technology and supported by a dedicated team of developers alongside a global community of volunteers, Thunderbird never treats your private data as a product.
Thunderbird for Android is based on K-9 Mail, which comes with a rich history of success and functionality in open source email.
## Download
Thunderbird for Android can be downloaded from a couple of sources:
- Thunderbird on [Google Play](https://play.google.com/store/apps/details?id=net.thunderbird.android&referrer=utm_campaign%3Dandroid_metadata%26utm_medium%3Dweb%26utm_source%3Dgithub.com%26utm_content%3Dlink) or [F-Droid](https://f-droid.org/packages/net.thunderbird.android)
- Thunderbird Beta on [Google Play](https://play.google.com/store/apps/details?id=net.thunderbird.android.beta&referrer=utm_campaign%3Dandroid_metadata%26utm_medium%3Dweb%26utm_source%3Dgithub.com%26utm_content%3Dlink) or [F-Droid](https://f-droid.org/packages/net.thunderbird.android.beta)
- [Github Releases](https://github.com/thunderbird/thunderbird-android/releases)
- [FFUpdater](https://f-droid.org/packages/de.marmaro.krt.ffupdater/) allows installing the latest versions from ftp.mozilla.org
By using Thunderbird for Android Beta, you have early access to current development and are able to try new features earlier.
Check out the [Release Notes](https://github.com/thunderbird/thunderbird-android/releases) to find out what changed in each version of Thunderbird for Android.
The SHA-256 fingerprints for our signing certificates are available in [SECURITY.md](./SECURITY.md#verifying-fingerprints).
## Need Help? Found a bug? Have an idea? Want to chat?
If the app is not behaving like it should, or you are not sure if you've encountered a bug:
- Check out our [knowledge base](https://support.mozilla.org/products/thunderbird-android) and [frequently asked questions](https://support.mozilla.org/kb/thunderbird-android-8-faq)
- Ask a question on our [support forum](https://support.mozilla.org/en-US/questions/new/thunderbird-android)
If you are certain you've identified a bug in Thunderbird for Android and would like to help fix it:
- File an issue on [our GitHub issue tracker](https://github.com/thunderbird/thunderbird-android/issues)
If you have an idea how to improve Thunderbird for Android:
- Tell us about and vote on your feature ideas on [connect.mozilla.org](https://connect.mozilla.org/t5/ideas/idb-p/ideas/label-name/thunderbird%20android).
- Join the discussion about the latest changes in the [Thunderbird Android Beta Topicbox](https://thunderbird.topicbox.com/groups/android-beta).
The Thunderbird Community uses Matrix to communicate:
- General chat about Thunderbird for Android and K-9 Mail: [#tb-android:mozilla.org](https://matrix.to/#/#tb-android:mozilla.org)
- Development and other ways to contribute: [#tb-android-dev:mozilla.org](https://matrix.to/#/#tb-android-dev:mozilla.org)
- Reach the broader Thunderbird Community in the [community space](https://matrix.to/#/#thunderbird-community:mozilla.org)
## Contributing
We welcome contributions from everyone.
- Development: Have you done a little bit of Kotlin? The [CONTRIBUTING](docs/CONTRIBUTING.md) guide will help you get started
- Translations: Do you speak a language aside from English? [Translating is easy](https://hosted.weblate.org/projects/tb-android/) and just takes a few minutes for your first success.
- We have [a number of other contribution opportunities](https://blog.thunderbird.net/2024/09/contribute-to-thunderbird-for-android/) available.
- Thunderbird is supported solely by financial contributions from users like you. [Make a financial contribution today](https://www.thunderbird.net/donate/mobile/?form=tfa)!
- Make sure to check out the [Mozilla Community Participation Guidelines](https://www.mozilla.org/about/governance/policies/participation/).
### Architecture Decision Records (ADR)
We use [Architecture Decision Records](https://adr.github.io/) to document the architectural decisions made in the
development of Thunderbird for Android. You can find them in the [`docs/architecture/adr`](docs/architecture/adr) directory.
For more information about our ADRs, please see the [ADRs README](docs/architecture/adr/README.md).
We encourage team members and contributors to read through our ADRs to understand the architectural decisions that
have shaped this project so far. Feel free to propose new ADRs or suggest modifications to existing ones as needed.
## K-9 Mail
In June 2022, [K-9 Mail joined the Thunderbird family](https://k9mail.app/2022/06/13/K-9-Mail-and-Thunderbird.html)
as the foundation for Thunderbird on Android. Since then, weve been updating both apps to give
users the same solid experience, so its normal to notice that K-9 Mail and Thunderbird look and
feel nearly identical. Theyre built on the same code, and thats intentional. You'll notice some
features are selectively enabled for Thunderbird as opposed to K-9 Mail, usually when they are
simply a better fit for Thunderbird (like the import from K-9 functionality).
If you prefer the robot dog and would like to keep K-9 Mail around, you can find it here:
- [K-9 Mail on Google Play](https://play.google.com/store/apps/details?id=com.fsck.k9&utm_source=thunderbird-android-github&utm_campaign=download-section)
- [K-9 Mail on F-Droid](https://f-droid.org/packages/com.fsck.k9/)
## Forking
If you want to use a fork of this project please ensure that you replace the OAuth client setup in the `app-k9mail/src/{debug,release}/kotlin/app/k9mail/auth/K9OAuthConfigurationFactory.kt` and `app-thunderbird/src/{debug,daily,beta,release}/kotlin/net/thunderbird/android/auth/TbOAuthConfigurationFactory.kt` with your own OAuth client setup and ensure that the `redirectUri` is different to the one used in the main project. This is to prevent conflicts with the main app when both are installed on the same device.
## License
Thunderbird for Android is licensed under the [Apache License, Version 2.0](LICENSE).

29
SECURITY.md Normal file
View file

@ -0,0 +1,29 @@
# Thunderbird for Android Security
## Security Audit
The code in this repository underwent an extensive security audit in collaboration with the Open Source Technology
Improvement Fund ([OSTIF](https://ostif.org/)) and [7ASecurity](https://7asecurity.com/) in the first half of 2023. For
more details, see
our [blog post](https://blog.thunderbird.net/2023/07/k-9-mail-collaborates-with-ostif-and-7asecurity-security-audit/).
## Verifying Fingerprints
These are the SHA-256 fingerprints for our signing certificates:
- Thunderbird: `B6:52:47:79:B3:DB:BC:5A:C1:7A:5A:C2:71:DD:B2:9D:CF:BF:72:35:78:C2:38:E0:3C:3C:21:78:11:35:6D:D1`
- Thunderbird Beta: `05:6B:FA:FB:45:02:49:50:2F:D9:22:62:28:70:4C:25:29:E1:B8:22:DA:06:76:0D:47:A8:5C:95:57:74:1F:BD`
- K-9 Mail: `55:C8:A5:23:B9:73:35:F5:BF:60:DF:E8:A9:F3:E1:DD:E7:44:51:6D:93:57:E8:0A:92:5B:7B:22:E4:F5:55:24`
You can use the following command to retrieve and [verify](https://developer.android.com/tools/apksigner#usage-verify)
the certificate before installation:
```bash
apksigner verify -v --print-certs <path-to-apk>
```
## Reporting Vulnerabilities
You can report a security vulnerability through the [vulnerability reporting form](https://github.com/thunderbird/thunderbird-android/security/advisories/new).
We appreciate your support in making Thunderbird for Android as safe as possible!

7
app-common/README.md Normal file
View file

@ -0,0 +1,7 @@
# App Common
# App Common
This is the central integration point for shared code among the K-9 Mail and Thunderbird for Android applications. Its purpose is to collect and organize the individual feature modules that contain the actual functionality, as well as the "glue code" and configurations that tie them together.
By keeping the shared code focused on these boundaries, we can ensure that it remains lean and avoids unnecessary dependencies. This approach allows us to maintain a clean and modular architecture, making it easier to maintain and update the codebase.

View file

@ -0,0 +1,48 @@
plugins {
id(ThunderbirdPlugins.Library.android)
}
android {
namespace = "net.thunderbird.app.common"
buildFeatures {
buildConfig = true
}
}
dependencies {
api(projects.legacy.common)
api(projects.legacy.ui.legacy)
api(projects.feature.account.core)
api(projects.feature.launcher)
api(projects.feature.navigation.drawer.api)
implementation(projects.legacy.core)
implementation(projects.core.android.account)
implementation(projects.core.logging.api)
implementation(projects.core.logging.implComposite)
implementation(projects.core.logging.implConsole)
implementation(projects.core.logging.implLegacy)
implementation(projects.core.logging.implFile)
implementation(projects.core.featureflag)
implementation(projects.core.ui.legacy.theme2.common)
implementation(projects.feature.account.avatar.api)
implementation(projects.feature.account.avatar.impl)
implementation(projects.feature.account.setup)
implementation(projects.feature.mail.account.api)
implementation(projects.feature.migration.provider)
implementation(projects.feature.notification.api)
implementation(projects.feature.notification.impl)
implementation(projects.feature.widget.messageList)
implementation(projects.mail.protocols.imap)
implementation(libs.androidx.work.runtime)
implementation(libs.androidx.lifecycle.process)
testImplementation(projects.feature.account.fake)
}

View file

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest
xmlns:android="http://schemas.android.com/apk/res/android"
android:installLocation="auto"
>
<application
android:allowBackup="false"
android:hasFragileUserData="false"
android:networkSecurityConfig="@xml/network_security_config"
android:usesCleartextTraffic="true"
/>
<queries>
<!-- Allow access to external text processing actions so they can be displayed in the text selection toolbar -->
<intent>
<action android:name="android.intent.action.PROCESS_TEXT" />
<data android:mimeType="text/plain" />
</intent>
</queries>
</manifest>

View file

@ -0,0 +1,22 @@
package net.thunderbird.app.common
import com.fsck.k9.legacyCommonAppModules
import com.fsck.k9.legacyCoreModules
import com.fsck.k9.legacyUiModules
import net.thunderbird.app.common.account.appCommonAccountModule
import net.thunderbird.app.common.core.appCommonCoreModule
import net.thunderbird.app.common.feature.appCommonFeatureModule
import org.koin.core.module.Module
import org.koin.dsl.module
val appCommonModule: Module = module {
includes(legacyCommonAppModules)
includes(legacyCoreModules)
includes(legacyUiModules)
includes(
appCommonAccountModule,
appCommonCoreModule,
appCommonFeatureModule,
)
}

View file

@ -0,0 +1,154 @@
package net.thunderbird.app.common
import android.app.Application
import android.content.Context
import android.content.res.Configuration
import android.content.res.Resources
import androidx.lifecycle.ProcessLifecycleOwner
import app.k9mail.feature.widget.message.list.MessageListWidgetManager
import app.k9mail.legacy.di.DI
import com.fsck.k9.Core
import com.fsck.k9.K9
import com.fsck.k9.MessagingListenerProvider
import com.fsck.k9.controller.MessagingController
import com.fsck.k9.job.WorkManagerConfigurationProvider
import com.fsck.k9.notification.NotificationChannelManager
import com.fsck.k9.ui.base.AppLanguageManager
import com.fsck.k9.ui.base.extensions.currentLocale
import java.util.Locale
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.drop
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import net.thunderbird.app.common.feature.LoggerLifecycleObserver
import net.thunderbird.core.common.exception.ExceptionHandler
import net.thunderbird.core.logging.Logger
import net.thunderbird.core.logging.file.FileLogSink
import net.thunderbird.core.logging.legacy.Log
import net.thunderbird.core.ui.theme.manager.ThemeManager
import org.koin.android.ext.android.inject
import org.koin.core.module.Module
import org.koin.core.qualifier.named
import androidx.work.Configuration as WorkManagerConfiguration
abstract class BaseApplication : Application(), WorkManagerConfiguration.Provider {
private val messagingController: MessagingController by inject()
private val messagingListenerProvider: MessagingListenerProvider by inject()
private val themeManager: ThemeManager by inject()
private val appLanguageManager: AppLanguageManager by inject()
private val notificationChannelManager: NotificationChannelManager by inject()
private val messageListWidgetManager: MessageListWidgetManager by inject()
private val workManagerConfigurationProvider: WorkManagerConfigurationProvider by inject()
private val logger: Logger by inject()
private val syncDebugFileLogSink: FileLogSink by inject(named("syncDebug"))
private val appCoroutineScope: CoroutineScope = MainScope()
private var appLanguageManagerInitialized = false
override fun attachBaseContext(base: Context?) {
Core.earlyInit()
// Start Koin early so it is ready by the time content providers are initialized.
DI.start(this, listOf(provideAppModule()))
Log.logger = logger
super.attachBaseContext(base)
}
override fun onCreate() {
super.onCreate()
K9.init(this)
Core.init(this)
initializeAppLanguage()
updateNotificationChannelsOnAppLanguageChanges()
themeManager.init()
messageListWidgetManager.init()
messagingListenerProvider.listeners.forEach { listener ->
messagingController.addListener(listener)
}
val originalHandler = Thread.getDefaultUncaughtExceptionHandler()
Thread.setDefaultUncaughtExceptionHandler(ExceptionHandler(originalHandler))
ProcessLifecycleOwner.get().lifecycle.addObserver(LoggerLifecycleObserver(syncDebugFileLogSink))
}
abstract fun provideAppModule(): Module
private fun initializeAppLanguage() {
appLanguageManager.init()
applyOverrideLocaleToConfiguration()
appLanguageManagerInitialized = true
listenForAppLanguageChanges()
}
private fun applyOverrideLocaleToConfiguration() {
appLanguageManager.getOverrideLocale()?.let { overrideLocale ->
updateConfigurationWithLocale(superResources.configuration, overrideLocale)
}
}
private fun listenForAppLanguageChanges() {
appLanguageManager.overrideLocale
.drop(1) // We already applied the initial value
.onEach { overrideLocale ->
val locale = overrideLocale ?: Locale.getDefault()
updateConfigurationWithLocale(superResources.configuration, locale)
}
.launchIn(appCoroutineScope)
}
override fun onConfigurationChanged(newConfiguration: Configuration) {
applyOverrideLocaleToConfiguration()
super.onConfigurationChanged(superResources.configuration)
}
private fun updateConfigurationWithLocale(configuration: Configuration, locale: Locale) {
Log.d("Updating application configuration with locale '$locale'")
val newConfiguration = Configuration(configuration).apply {
currentLocale = locale
}
@Suppress("DEPRECATION")
superResources.updateConfiguration(newConfiguration, superResources.displayMetrics)
}
private val superResources: Resources
get() = super.getResources()
// Creating a WebView instance triggers something that will cause the configuration of the Application's Resources
// instance to be reset to the default, i.e. not containing our locale override. Unfortunately, we're not notified
// about this event. So we're checking each time someone asks for the Resources instance whether we need to change
// the configuration again. Luckily, right now (Android 11), the platform is calling this method right after
// resetting the configuration.
override fun getResources(): Resources {
val resources = super.getResources()
if (appLanguageManagerInitialized) {
appLanguageManager.getOverrideLocale()?.let { overrideLocale ->
if (resources.configuration.currentLocale != overrideLocale) {
Log.w("Resources configuration was reset. Re-applying locale override.")
appLanguageManager.applyOverrideLocale()
applyOverrideLocaleToConfiguration()
}
}
}
return resources
}
private fun updateNotificationChannelsOnAppLanguageChanges() {
appLanguageManager.appLocale
.distinctUntilChanged()
.onEach { notificationChannelManager.updateChannels() }
.launchIn(appCoroutineScope)
}
override val workManagerConfiguration: WorkManagerConfiguration
get() = workManagerConfigurationProvider.getConfiguration()
}

View file

@ -0,0 +1,27 @@
package net.thunderbird.app.common.account
import android.content.res.Resources
import app.k9mail.core.ui.legacy.theme2.common.R
import net.thunderbird.core.android.account.AccountManager
internal class AccountColorPicker(
private val accountManager: AccountManager,
private val resources: Resources,
) {
fun pickColor(): Int {
val accounts = accountManager.getAccounts()
val usedAccountColors = accounts.map { it.chipColor }.toSet()
val accountColors = resources.getIntArray(R.array.account_colors).toList()
val availableColors = accountColors - usedAccountColors
if (availableColors.isEmpty()) {
return accountColors.random()
}
val defaultAccountColors = resources.getIntArray(R.array.default_account_colors)
return availableColors.shuffled().minByOrNull { color ->
val index = defaultAccountColors.indexOf(color)
if (index != -1) index else defaultAccountColors.size
} ?: error("availableColors must not be empty")
}
}

View file

@ -0,0 +1,169 @@
package net.thunderbird.app.common.account
import android.content.Context
import app.k9mail.feature.account.common.domain.entity.Account
import app.k9mail.feature.account.common.domain.entity.SpecialFolderOption
import app.k9mail.feature.account.common.domain.entity.SpecialFolderSettings
import app.k9mail.feature.account.setup.AccountSetupExternalContract
import app.k9mail.feature.account.setup.AccountSetupExternalContract.AccountCreator.AccountCreatorResult
import com.fsck.k9.Core
import com.fsck.k9.Preferences
import com.fsck.k9.account.DeletePolicyProvider
import com.fsck.k9.controller.MessagingController
import com.fsck.k9.mail.ServerSettings
import com.fsck.k9.mail.store.imap.ImapStoreSettings.autoDetectNamespace
import com.fsck.k9.mail.store.imap.ImapStoreSettings.createExtra
import com.fsck.k9.mail.store.imap.ImapStoreSettings.isSendClientInfo
import com.fsck.k9.mail.store.imap.ImapStoreSettings.isUseCompression
import com.fsck.k9.mail.store.imap.ImapStoreSettings.pathPrefix
import com.fsck.k9.mailstore.SpecialLocalFoldersCreator
import com.fsck.k9.preferences.UnifiedInboxConfigurator
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import net.thunderbird.core.android.account.LegacyAccount
import net.thunderbird.core.common.mail.Protocols
import net.thunderbird.core.logging.legacy.Log
import net.thunderbird.feature.account.avatar.AvatarMonogramCreator
import net.thunderbird.feature.account.storage.profile.AvatarDto
import net.thunderbird.feature.account.storage.profile.AvatarTypeDto
import net.thunderbird.feature.mail.folder.api.SpecialFolderSelection
// TODO Move to feature/account/setup
@Suppress("LongParameterList")
internal class AccountCreator(
private val accountColorPicker: AccountColorPicker,
private val localFoldersCreator: SpecialLocalFoldersCreator,
private val preferences: Preferences,
private val context: Context,
private val messagingController: MessagingController,
private val deletePolicyProvider: DeletePolicyProvider,
private val avatarMonogramCreator: AvatarMonogramCreator,
private val unifiedInboxConfigurator: UnifiedInboxConfigurator,
private val coroutineDispatcher: CoroutineDispatcher = Dispatchers.IO,
) : AccountSetupExternalContract.AccountCreator {
@Suppress("TooGenericExceptionCaught")
override suspend fun createAccount(account: Account): AccountCreatorResult {
return try {
withContext(coroutineDispatcher) { AccountCreatorResult.Success(create(account)) }
} catch (e: Exception) {
Log.e(e, "Error while creating new account")
AccountCreatorResult.Error(e.message ?: "Unknown create account error")
}
}
private suspend fun create(account: Account): String {
val newAccount = preferences.newAccount(account.uuid)
newAccount.email = account.emailAddress
newAccount.avatar = AvatarDto(
avatarType = AvatarTypeDto.MONOGRAM,
avatarMonogram = avatarMonogramCreator.create(account.options.accountName, account.emailAddress),
avatarImageUri = null,
avatarIconName = null,
)
newAccount.setIncomingServerSettings(account.incomingServerSettings)
newAccount.outgoingServerSettings = account.outgoingServerSettings
newAccount.oAuthState = account.authorizationState
newAccount.name = account.options.accountName
newAccount.senderName = account.options.displayName
if (account.options.emailSignature != null) {
newAccount.signatureUse = true
newAccount.signature = account.options.emailSignature
}
newAccount.isNotifyNewMail = account.options.showNotification
newAccount.automaticCheckIntervalMinutes = account.options.checkFrequencyInMinutes
newAccount.displayCount = account.options.messageDisplayCount
newAccount.deletePolicy = deletePolicyProvider.getDeletePolicy(newAccount.incomingServerSettings.type)
newAccount.chipColor = accountColorPicker.pickColor()
localFoldersCreator.createSpecialLocalFolders(newAccount)
account.specialFolderSettings?.let { specialFolderSettings ->
newAccount.setSpecialFolders(specialFolderSettings)
}
newAccount.markSetupFinished()
preferences.saveAccount(newAccount)
unifiedInboxConfigurator.configureUnifiedInbox()
Core.setServicesEnabled(context)
messagingController.refreshFolderListBlocking(newAccount)
if (account.options.checkFrequencyInMinutes == -1) {
messagingController.checkMail(newAccount, false, true, false, null)
}
return newAccount.uuid
}
/**
* Set special folders by name.
*
* Since the folder list hasn't been synced yet, we don't have database IDs for the folders. So we use the same
* mechanism that is used when importing settings. See [com.fsck.k9.mailstore.SpecialFolderUpdater] for details.
*/
private fun LegacyAccount.setSpecialFolders(specialFolders: SpecialFolderSettings) {
importedArchiveFolder = specialFolders.archiveSpecialFolderOption.toFolderServerId()
archiveFolderSelection = specialFolders.archiveSpecialFolderOption.toFolderSelection()
importedDraftsFolder = specialFolders.draftsSpecialFolderOption.toFolderServerId()
draftsFolderSelection = specialFolders.draftsSpecialFolderOption.toFolderSelection()
importedSentFolder = specialFolders.sentSpecialFolderOption.toFolderServerId()
sentFolderSelection = specialFolders.sentSpecialFolderOption.toFolderSelection()
importedSpamFolder = specialFolders.spamSpecialFolderOption.toFolderServerId()
spamFolderSelection = specialFolders.spamSpecialFolderOption.toFolderSelection()
importedTrashFolder = specialFolders.trashSpecialFolderOption.toFolderServerId()
trashFolderSelection = specialFolders.trashSpecialFolderOption.toFolderSelection()
}
private fun SpecialFolderOption.toFolderServerId(): String? {
return when (this) {
is SpecialFolderOption.None -> null
is SpecialFolderOption.Regular -> remoteFolder.serverId.serverId
is SpecialFolderOption.Special -> remoteFolder.serverId.serverId
}
}
private fun SpecialFolderOption.toFolderSelection(): SpecialFolderSelection {
return when (this) {
is SpecialFolderOption.None -> {
if (isAutomatic) SpecialFolderSelection.AUTOMATIC else SpecialFolderSelection.MANUAL
}
is SpecialFolderOption.Regular -> {
SpecialFolderSelection.MANUAL
}
is SpecialFolderOption.Special -> {
if (isAutomatic) SpecialFolderSelection.AUTOMATIC else SpecialFolderSelection.MANUAL
}
}
}
}
private fun LegacyAccount.setIncomingServerSettings(serverSettings: ServerSettings) {
if (serverSettings.type == Protocols.IMAP) {
useCompression = serverSettings.isUseCompression
isSendClientInfoEnabled = serverSettings.isSendClientInfo
incomingServerSettings = serverSettings.copy(
extra = createExtra(
autoDetectNamespace = serverSettings.autoDetectNamespace,
pathPrefix = serverSettings.pathPrefix,
),
)
} else {
incomingServerSettings = serverSettings
}
}

View file

@ -0,0 +1,66 @@
package net.thunderbird.app.common.account
import app.k9mail.feature.account.setup.AccountSetupExternalContract
import net.thunderbird.app.common.account.data.DefaultAccountProfileLocalDataSource
import net.thunderbird.app.common.account.data.DefaultLegacyAccountWrapperManager
import net.thunderbird.core.android.account.AccountDefaultsProvider
import net.thunderbird.core.android.account.LegacyAccountWrapperManager
import net.thunderbird.feature.account.avatar.AvatarMonogramCreator
import net.thunderbird.feature.account.avatar.DefaultAvatarMonogramCreator
import net.thunderbird.feature.account.core.AccountCoreExternalContract.AccountProfileLocalDataSource
import net.thunderbird.feature.account.core.featureAccountCoreModule
import net.thunderbird.feature.account.storage.legacy.featureAccountStorageLegacyModule
import org.koin.android.ext.koin.androidApplication
import org.koin.dsl.module
internal val appCommonAccountModule = module {
includes(
featureAccountCoreModule,
featureAccountStorageLegacyModule,
)
single<LegacyAccountWrapperManager> {
DefaultLegacyAccountWrapperManager(
accountManager = get(),
accountDataMapper = get(),
)
}
single<AccountProfileLocalDataSource> {
DefaultAccountProfileLocalDataSource(
accountManager = get(),
dataMapper = get(),
)
}
single<AccountDefaultsProvider> {
DefaultAccountDefaultsProvider(
resourceProvider = get(),
featureFlagProvider = get(),
)
}
factory {
AccountColorPicker(
accountManager = get(),
resources = get(),
)
}
factory<AvatarMonogramCreator> {
DefaultAvatarMonogramCreator()
}
factory<AccountSetupExternalContract.AccountCreator> {
AccountCreator(
accountColorPicker = get(),
localFoldersCreator = get(),
preferences = get(),
context = androidApplication(),
deletePolicyProvider = get(),
messagingController = get(),
avatarMonogramCreator = get(),
unifiedInboxConfigurator = get(),
)
}
}

View file

@ -0,0 +1,138 @@
package net.thunderbird.app.common.account
import com.fsck.k9.CoreResourceProvider
import net.thunderbird.core.android.account.AccountDefaultsProvider
import net.thunderbird.core.android.account.AccountDefaultsProvider.Companion.DEFAULT_MAXIMUM_AUTO_DOWNLOAD_MESSAGE_SIZE
import net.thunderbird.core.android.account.AccountDefaultsProvider.Companion.DEFAULT_MESSAGE_FORMAT
import net.thunderbird.core.android.account.AccountDefaultsProvider.Companion.DEFAULT_MESSAGE_FORMAT_AUTO
import net.thunderbird.core.android.account.AccountDefaultsProvider.Companion.DEFAULT_MESSAGE_READ_RECEIPT
import net.thunderbird.core.android.account.AccountDefaultsProvider.Companion.DEFAULT_QUOTED_TEXT_SHOWN
import net.thunderbird.core.android.account.AccountDefaultsProvider.Companion.DEFAULT_QUOTE_PREFIX
import net.thunderbird.core.android.account.AccountDefaultsProvider.Companion.DEFAULT_QUOTE_STYLE
import net.thunderbird.core.android.account.AccountDefaultsProvider.Companion.DEFAULT_REMOTE_SEARCH_NUM_RESULTS
import net.thunderbird.core.android.account.AccountDefaultsProvider.Companion.DEFAULT_REPLY_AFTER_QUOTE
import net.thunderbird.core.android.account.AccountDefaultsProvider.Companion.DEFAULT_RINGTONE_URI
import net.thunderbird.core.android.account.AccountDefaultsProvider.Companion.DEFAULT_SORT_ASCENDING
import net.thunderbird.core.android.account.AccountDefaultsProvider.Companion.DEFAULT_SORT_TYPE
import net.thunderbird.core.android.account.AccountDefaultsProvider.Companion.DEFAULT_STRIP_SIGNATURE
import net.thunderbird.core.android.account.AccountDefaultsProvider.Companion.DEFAULT_SYNC_INTERVAL
import net.thunderbird.core.android.account.AccountDefaultsProvider.Companion.DEFAULT_VISIBLE_LIMIT
import net.thunderbird.core.android.account.AccountDefaultsProvider.Companion.NO_OPENPGP_KEY
import net.thunderbird.core.android.account.AccountDefaultsProvider.Companion.UNASSIGNED_ACCOUNT_NUMBER
import net.thunderbird.core.android.account.Expunge
import net.thunderbird.core.android.account.FolderMode
import net.thunderbird.core.android.account.Identity
import net.thunderbird.core.android.account.LegacyAccount
import net.thunderbird.core.android.account.ShowPictures
import net.thunderbird.core.featureflag.FeatureFlagProvider
import net.thunderbird.core.featureflag.toFeatureFlagKey
import net.thunderbird.core.preference.storage.Storage
import net.thunderbird.feature.mail.folder.api.SpecialFolderSelection
import net.thunderbird.feature.notification.NotificationLight
import net.thunderbird.feature.notification.NotificationSettings
import net.thunderbird.feature.notification.NotificationVibration
@Suppress("MagicNumber")
internal class DefaultAccountDefaultsProvider(
private val resourceProvider: CoreResourceProvider,
private val featureFlagProvider: FeatureFlagProvider,
) : AccountDefaultsProvider {
override fun applyDefaults(account: LegacyAccount) = with(account) {
applyLegacyDefaults()
}
override fun applyOverwrites(account: LegacyAccount, storage: Storage) = with(account) {
if (storage.contains("${account.uuid}.notifyNewMail")) {
isNotifyNewMail = storage.getBoolean("${account.uuid}.notifyNewMail", false)
isNotifySelfNewMail = storage.getBoolean("${account.uuid}.notifySelfNewMail", true)
} else {
isNotifyNewMail = featureFlagProvider.provide(
"email_notification_default".toFeatureFlagKey(),
).whenEnabledOrNot(
onEnabled = { true },
onDisabledOrUnavailable = { false },
)
isNotifySelfNewMail = featureFlagProvider.provide(
"email_notification_default".toFeatureFlagKey(),
).whenEnabledOrNot(
onEnabled = { true },
onDisabledOrUnavailable = { false },
)
}
}
@Suppress("LongMethod")
private fun LegacyAccount.applyLegacyDefaults() {
automaticCheckIntervalMinutes = DEFAULT_SYNC_INTERVAL
idleRefreshMinutes = 24
displayCount = 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
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 = DEFAULT_MAXIMUM_AUTO_DOWNLOAD_MESSAGE_SIZE
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)
identities = ArrayList<Identity>()
val identity = Identity(
signatureUse = false,
signature = null,
description = resourceProvider.defaultIdentityDescription(),
)
identities.add(identity)
updateNotificationSettings {
NotificationSettings(
isRingEnabled = true,
ringtone = DEFAULT_RINGTONE_URI,
light = NotificationLight.Disabled,
vibration = NotificationVibration.DEFAULT,
)
}
resetChangeMarkers()
}
}

View file

@ -0,0 +1,38 @@
package net.thunderbird.app.common.account.data
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.flow.map
import net.thunderbird.core.android.account.LegacyAccountWrapperManager
import net.thunderbird.feature.account.AccountId
import net.thunderbird.feature.account.core.AccountCoreExternalContract.AccountProfileLocalDataSource
import net.thunderbird.feature.account.profile.AccountProfile
import net.thunderbird.feature.account.storage.mapper.AccountProfileDataMapper
internal class DefaultAccountProfileLocalDataSource(
private val accountManager: LegacyAccountWrapperManager,
private val dataMapper: AccountProfileDataMapper,
) : AccountProfileLocalDataSource {
override fun getById(accountId: AccountId): Flow<AccountProfile?> {
return accountManager.getById(accountId)
.map { account ->
account?.let { dto ->
dataMapper.toDomain(dto.profile)
}
}
}
override suspend fun update(accountProfile: AccountProfile) {
val currentAccount = accountManager.getById(accountProfile.id)
.firstOrNull() ?: return
val accountProfile = dataMapper.toDto(accountProfile)
val updatedAccount = currentAccount.copy(
profile = accountProfile,
)
accountManager.update(updatedAccount)
}
}

View file

@ -0,0 +1,38 @@
package net.thunderbird.app.common.account.data
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import net.thunderbird.core.android.account.AccountManager
import net.thunderbird.core.android.account.LegacyAccountWrapper
import net.thunderbird.core.android.account.LegacyAccountWrapperManager
import net.thunderbird.feature.account.AccountId
import net.thunderbird.feature.account.storage.legacy.mapper.DefaultLegacyAccountWrapperDataMapper
internal class DefaultLegacyAccountWrapperManager(
private val accountManager: AccountManager,
private val accountDataMapper: DefaultLegacyAccountWrapperDataMapper,
) : LegacyAccountWrapperManager {
override fun getAll(): Flow<List<LegacyAccountWrapper>> {
return accountManager.getAccountsFlow()
.map { list ->
list.map { account ->
accountDataMapper.toDomain(account)
}
}
}
override fun getById(id: AccountId): Flow<LegacyAccountWrapper?> {
return accountManager.getAccountFlow(id.asRaw()).map { account ->
account?.let {
accountDataMapper.toDomain(it)
}
}
}
override suspend fun update(account: LegacyAccountWrapper) {
accountManager.saveAccount(
accountDataMapper.toDto(account),
)
}
}

View file

@ -0,0 +1,70 @@
package net.thunderbird.app.common.core
import android.content.Context
import kotlin.time.ExperimentalTime
import net.thunderbird.app.common.core.logging.DefaultLogLevelManager
import net.thunderbird.core.common.inject.getList
import net.thunderbird.core.common.inject.singleListOf
import net.thunderbird.core.logging.DefaultLogger
import net.thunderbird.core.logging.LogLevel
import net.thunderbird.core.logging.LogLevelManager
import net.thunderbird.core.logging.LogLevelProvider
import net.thunderbird.core.logging.LogSink
import net.thunderbird.core.logging.Logger
import net.thunderbird.core.logging.composite.CompositeLogSink
import net.thunderbird.core.logging.console.ConsoleLogSink
import net.thunderbird.core.logging.file.AndroidFileSystemManager
import net.thunderbird.core.logging.file.FileLogSink
import org.koin.core.module.Module
import org.koin.core.qualifier.named
import org.koin.dsl.bind
import org.koin.dsl.module
val appCommonCoreModule: Module = module {
single<LogLevelManager> {
DefaultLogLevelManager()
}.bind<LogLevelProvider>()
singleListOf<LogSink>(
{ ConsoleLogSink(level = LogLevel.VERBOSE) },
)
single<CompositeLogSink> {
CompositeLogSink(
logLevelProvider = get(),
sinks = getList(),
)
}
single<Logger> {
@OptIn(ExperimentalTime::class)
DefaultLogger(
sink = get<CompositeLogSink>(),
)
}
single<CompositeLogSink>(named(SYNC_DEBUG_LOG)) {
CompositeLogSink(
logLevelProvider = get(),
sinks = getList(),
)
}
single<FileLogSink>(named(SYNC_DEBUG_LOG)) {
FileLogSink(
level = LogLevel.DEBUG,
fileName = "thunderbird-sync-debug",
fileLocation = get<Context>().filesDir.path,
fileSystemManager = AndroidFileSystemManager(get<Context>().contentResolver),
)
}
single<Logger>(named(SYNC_DEBUG_LOG)) {
@OptIn(ExperimentalTime::class)
DefaultLogger(
sink = get<CompositeLogSink>(named(SYNC_DEBUG_LOG)),
)
}
}
internal const val SYNC_DEBUG_LOG = "syncDebug"

View file

@ -0,0 +1,22 @@
package net.thunderbird.app.common.core.logging
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.update
import net.thunderbird.app.common.BuildConfig
import net.thunderbird.core.logging.LogLevel
import net.thunderbird.core.logging.LogLevelManager
class DefaultLogLevelManager : LogLevelManager {
private val defaultLevel = if (BuildConfig.DEBUG) LogLevel.VERBOSE else LogLevel.INFO
private val logLevel = MutableStateFlow(defaultLevel)
override fun override(level: LogLevel) {
logLevel.update { level }
}
override fun restoreDefault() {
override(defaultLevel)
}
override fun current(): LogLevel = logLevel.value
}

View file

@ -0,0 +1,17 @@
package net.thunderbird.app.common.feature
import android.content.Context
import app.k9mail.feature.launcher.FeatureLauncherExternalContract
import com.fsck.k9.activity.MessageList
internal class AccountSetupFinishedLauncher(
private val context: Context,
) : FeatureLauncherExternalContract.AccountSetupFinishedLauncher {
override fun launch(accountUuid: String?) {
if (accountUuid != null) {
MessageList.launch(context, accountUuid)
} else {
MessageList.launch(context)
}
}
}

View file

@ -0,0 +1,27 @@
package net.thunderbird.app.common.feature
import app.k9mail.feature.launcher.FeatureLauncherExternalContract
import app.k9mail.feature.launcher.di.featureLauncherModule
import net.thunderbird.feature.navigation.drawer.api.NavigationDrawerExternalContract
import net.thunderbird.feature.notification.impl.inject.featureNotificationModule
import org.koin.android.ext.koin.androidContext
import org.koin.dsl.module
internal val appCommonFeatureModule = module {
includes(featureLauncherModule)
includes(featureNotificationModule)
factory<FeatureLauncherExternalContract.AccountSetupFinishedLauncher> {
AccountSetupFinishedLauncher(
context = androidContext(),
)
}
single<NavigationDrawerExternalContract.DrawerConfigLoader> {
NavigationDrawerConfigLoader(get())
}
single<NavigationDrawerExternalContract.DrawerConfigWriter> {
NavigationDrawerConfigWriter(get())
}
}

View file

@ -0,0 +1,22 @@
package net.thunderbird.app.common.feature
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import net.thunderbird.core.logging.file.FileLogSink
class LoggerLifecycleObserver(val fileLogSink: FileLogSink?) : DefaultLifecycleObserver {
override fun onStop(owner: LifecycleOwner) {
super.onStop(owner)
fileLogSink?.let {
owner.lifecycleScope.launch {
withContext(Dispatchers.IO) {
it.flushAndCloseBuffer()
}
}
}
}
}

View file

@ -0,0 +1,12 @@
package net.thunderbird.app.common.feature
import com.fsck.k9.preferences.DrawerConfigManager
import kotlinx.coroutines.flow.Flow
import net.thunderbird.feature.navigation.drawer.api.NavigationDrawerExternalContract
internal class NavigationDrawerConfigLoader(private val drawerConfigManager: DrawerConfigManager) :
NavigationDrawerExternalContract.DrawerConfigLoader {
override fun loadDrawerConfigFlow(): Flow<NavigationDrawerExternalContract.DrawerConfig> {
return drawerConfigManager.getConfigFlow()
}
}

View file

@ -0,0 +1,13 @@
package net.thunderbird.app.common.feature
import com.fsck.k9.preferences.DrawerConfigManager
import net.thunderbird.feature.navigation.drawer.api.NavigationDrawerExternalContract
import net.thunderbird.feature.navigation.drawer.api.NavigationDrawerExternalContract.DrawerConfig
internal class NavigationDrawerConfigWriter(
private val drawerConfigManager: DrawerConfigManager,
) : NavigationDrawerExternalContract.DrawerConfigWriter {
override fun writeDrawerConfig(drawerConfig: DrawerConfig) {
drawerConfigManager.save(drawerConfig)
}
}

View file

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<network-security-config
xmlns:tools="http://schemas.android.com/tools"
tools:ignore="InsecureBaseConfiguration,AcceptsUserCertificates"
>
<base-config cleartextTrafficPermitted="true">
<trust-anchors>
<certificates src="system" />
<certificates src="user" />
</trust-anchors>
</base-config>
</network-security-config>

View file

@ -0,0 +1,262 @@
package net.thunderbird.app.common.account
import assertk.assertThat
import assertk.assertions.isEqualTo
import assertk.assertions.isFalse
import assertk.assertions.isNull
import assertk.assertions.isTrue
import com.fsck.k9.CoreResourceProvider
import net.thunderbird.core.android.account.AccountDefaultsProvider.Companion.DEFAULT_MAXIMUM_AUTO_DOWNLOAD_MESSAGE_SIZE
import net.thunderbird.core.android.account.AccountDefaultsProvider.Companion.DEFAULT_MESSAGE_FORMAT
import net.thunderbird.core.android.account.AccountDefaultsProvider.Companion.DEFAULT_MESSAGE_FORMAT_AUTO
import net.thunderbird.core.android.account.AccountDefaultsProvider.Companion.DEFAULT_MESSAGE_READ_RECEIPT
import net.thunderbird.core.android.account.AccountDefaultsProvider.Companion.DEFAULT_QUOTED_TEXT_SHOWN
import net.thunderbird.core.android.account.AccountDefaultsProvider.Companion.DEFAULT_QUOTE_PREFIX
import net.thunderbird.core.android.account.AccountDefaultsProvider.Companion.DEFAULT_QUOTE_STYLE
import net.thunderbird.core.android.account.AccountDefaultsProvider.Companion.DEFAULT_REMOTE_SEARCH_NUM_RESULTS
import net.thunderbird.core.android.account.AccountDefaultsProvider.Companion.DEFAULT_REPLY_AFTER_QUOTE
import net.thunderbird.core.android.account.AccountDefaultsProvider.Companion.DEFAULT_RINGTONE_URI
import net.thunderbird.core.android.account.AccountDefaultsProvider.Companion.DEFAULT_SORT_ASCENDING
import net.thunderbird.core.android.account.AccountDefaultsProvider.Companion.DEFAULT_SORT_TYPE
import net.thunderbird.core.android.account.AccountDefaultsProvider.Companion.DEFAULT_STRIP_SIGNATURE
import net.thunderbird.core.android.account.AccountDefaultsProvider.Companion.DEFAULT_SYNC_INTERVAL
import net.thunderbird.core.android.account.AccountDefaultsProvider.Companion.DEFAULT_VISIBLE_LIMIT
import net.thunderbird.core.android.account.AccountDefaultsProvider.Companion.NO_OPENPGP_KEY
import net.thunderbird.core.android.account.AccountDefaultsProvider.Companion.UNASSIGNED_ACCOUNT_NUMBER
import net.thunderbird.core.android.account.Expunge
import net.thunderbird.core.android.account.FolderMode
import net.thunderbird.core.android.account.Identity
import net.thunderbird.core.android.account.LegacyAccount
import net.thunderbird.core.android.account.ShowPictures
import net.thunderbird.core.featureflag.FeatureFlagResult
import net.thunderbird.core.preference.storage.Storage
import net.thunderbird.feature.mail.folder.api.SpecialFolderSelection
import net.thunderbird.feature.notification.NotificationLight
import net.thunderbird.feature.notification.NotificationSettings
import net.thunderbird.feature.notification.NotificationVibration
import org.junit.Test
import org.mockito.kotlin.doReturn
import org.mockito.kotlin.mock
class DefaultAccountDefaultsProviderTest {
@Suppress("LongMethod")
@Test
fun `applyDefaults should return default values`() {
// arrange
val resourceProvider = mock<CoreResourceProvider> {
on { defaultIdentityDescription() } doReturn "Default Identity"
}
val account = LegacyAccount(
uuid = "cf728064-077d-4369-a0c7-7c2b21693d9b",
isSensitiveDebugLoggingEnabled = { false },
)
val identities = listOf(
Identity(
signatureUse = false,
signature = null,
description = resourceProvider.defaultIdentityDescription(),
),
)
val notificationSettings = NotificationSettings(
isRingEnabled = true,
ringtone = DEFAULT_RINGTONE_URI,
light = NotificationLight.Disabled,
vibration = NotificationVibration.DEFAULT,
)
val testSubject = DefaultAccountDefaultsProvider(
resourceProvider = resourceProvider,
featureFlagProvider = {
FeatureFlagResult.Disabled
},
)
// act
testSubject.applyDefaults(account)
// assert
assertThat(account.automaticCheckIntervalMinutes).isEqualTo(DEFAULT_SYNC_INTERVAL)
assertThat(account.idleRefreshMinutes).isEqualTo(24)
assertThat(account.displayCount).isEqualTo(DEFAULT_VISIBLE_LIMIT)
assertThat(account.accountNumber).isEqualTo(UNASSIGNED_ACCOUNT_NUMBER)
assertThat(account.isNotifyNewMail).isTrue()
assertThat(account.folderNotifyNewMailMode).isEqualTo(FolderMode.ALL)
assertThat(account.isNotifySync).isFalse()
assertThat(account.isNotifySelfNewMail).isTrue()
assertThat(account.isNotifyContactsMailOnly).isFalse()
assertThat(account.isIgnoreChatMessages).isFalse()
assertThat(account.messagesNotificationChannelVersion).isEqualTo(0)
assertThat(account.folderDisplayMode).isEqualTo(FolderMode.NOT_SECOND_CLASS)
assertThat(account.folderSyncMode).isEqualTo(FolderMode.FIRST_CLASS)
assertThat(account.folderPushMode).isEqualTo(FolderMode.NONE)
assertThat(account.sortType).isEqualTo(DEFAULT_SORT_TYPE)
assertThat(account.isSortAscending(DEFAULT_SORT_TYPE)).isEqualTo(DEFAULT_SORT_ASCENDING)
assertThat(account.showPictures).isEqualTo(ShowPictures.NEVER)
assertThat(account.isSignatureBeforeQuotedText).isFalse()
assertThat(account.expungePolicy).isEqualTo(Expunge.EXPUNGE_IMMEDIATELY)
assertThat(account.importedAutoExpandFolder).isNull()
assertThat(account.legacyInboxFolder).isNull()
assertThat(account.maxPushFolders).isEqualTo(10)
assertThat(account.isSubscribedFoldersOnly).isFalse()
assertThat(account.maximumPolledMessageAge).isEqualTo(-1)
assertThat(account.maximumAutoDownloadMessageSize).isEqualTo(DEFAULT_MAXIMUM_AUTO_DOWNLOAD_MESSAGE_SIZE)
assertThat(account.messageFormat).isEqualTo(DEFAULT_MESSAGE_FORMAT)
assertThat(account.isMessageFormatAuto).isEqualTo(DEFAULT_MESSAGE_FORMAT_AUTO)
assertThat(account.isMessageReadReceipt).isEqualTo(DEFAULT_MESSAGE_READ_RECEIPT)
assertThat(account.quoteStyle).isEqualTo(DEFAULT_QUOTE_STYLE)
assertThat(account.quotePrefix).isEqualTo(DEFAULT_QUOTE_PREFIX)
assertThat(account.isDefaultQuotedTextShown).isEqualTo(DEFAULT_QUOTED_TEXT_SHOWN)
assertThat(account.isReplyAfterQuote).isEqualTo(DEFAULT_REPLY_AFTER_QUOTE)
assertThat(account.isStripSignature).isEqualTo(DEFAULT_STRIP_SIGNATURE)
assertThat(account.isSyncRemoteDeletions).isTrue()
assertThat(account.openPgpKey).isEqualTo(NO_OPENPGP_KEY)
assertThat(account.isRemoteSearchFullText).isFalse()
assertThat(account.remoteSearchNumResults).isEqualTo(DEFAULT_REMOTE_SEARCH_NUM_RESULTS)
assertThat(account.isUploadSentMessages).isTrue()
assertThat(account.isMarkMessageAsReadOnView).isTrue()
assertThat(account.isMarkMessageAsReadOnDelete).isTrue()
assertThat(account.isAlwaysShowCcBcc).isFalse()
assertThat(account.lastSyncTime).isEqualTo(0L)
assertThat(account.lastFolderListRefreshTime).isEqualTo(0L)
assertThat(account.archiveFolderId).isNull()
assertThat(account.archiveFolderSelection).isEqualTo(SpecialFolderSelection.AUTOMATIC)
assertThat(account.draftsFolderId).isNull()
assertThat(account.draftsFolderSelection).isEqualTo(SpecialFolderSelection.AUTOMATIC)
assertThat(account.sentFolderId).isNull()
assertThat(account.sentFolderSelection).isEqualTo(SpecialFolderSelection.AUTOMATIC)
assertThat(account.spamFolderId).isNull()
assertThat(account.spamFolderSelection).isEqualTo(SpecialFolderSelection.AUTOMATIC)
assertThat(account.trashFolderId).isNull()
assertThat(account.trashFolderSelection).isEqualTo(SpecialFolderSelection.AUTOMATIC)
assertThat(account.archiveFolderId).isNull()
assertThat(account.archiveFolderSelection).isEqualTo(SpecialFolderSelection.AUTOMATIC)
assertThat(account.identities).isEqualTo(identities)
assertThat(account.notificationSettings).isEqualTo(notificationSettings)
assertThat(account.isChangedVisibleLimits).isFalse()
}
@Test
fun `applyOverwrites should return patched account when disabled`() {
// arrange
val resourceProvider = mock<CoreResourceProvider> {
on { defaultIdentityDescription() } doReturn "Default Identity"
}
val account = LegacyAccount(
uuid = "cf728064-077d-4369-a0c7-7c2b21693d9b",
isSensitiveDebugLoggingEnabled = { false },
)
val storage = mock<Storage> {
on { contains("${account.uuid}.notifyNewMail") } doReturn false
on { getBoolean("${account.uuid}.notifyNewMail", false) } doReturn false
on { getBoolean("${account.uuid}.notifySelfNewMail", false) } doReturn false
}
val testSubject = DefaultAccountDefaultsProvider(
resourceProvider = resourceProvider,
featureFlagProvider = {
FeatureFlagResult.Disabled
},
)
// act
testSubject.applyOverwrites(account, storage)
// assert
assertThat(account.isNotifyNewMail).isFalse()
assertThat(account.isNotifySelfNewMail).isFalse()
}
@Test
fun `applyOverwrites should return patched account when enabled`() {
// arrange
val resourceProvider = mock<CoreResourceProvider> {
on { defaultIdentityDescription() } doReturn "Default Identity"
}
val account = LegacyAccount(
uuid = "cf728064-077d-4369-a0c7-7c2b21693d9b",
isSensitiveDebugLoggingEnabled = { false },
)
val storage = mock<Storage> {
on { contains("${account.uuid}.notifyNewMail") } doReturn false
on { getBoolean("${account.uuid}.notifyNewMail", false) } doReturn false
on { getBoolean("${account.uuid}.notifySelfNewMail", false) } doReturn false
}
val testSubject = DefaultAccountDefaultsProvider(
resourceProvider = resourceProvider,
featureFlagProvider = {
FeatureFlagResult.Enabled
},
)
// act
testSubject.applyOverwrites(account, storage)
// assert
assertThat(account.isNotifyNewMail).isTrue()
assertThat(account.isNotifySelfNewMail).isTrue()
}
@Suppress("MaxLineLength")
@Test
fun `applyOverwrites updates account notification values from storage when storage contains isNotifyNewMail value`() {
// arrange
val resourceProvider = mock<CoreResourceProvider> {
on { defaultIdentityDescription() } doReturn "Default Identity"
}
val account = LegacyAccount(
uuid = "cf728064-077d-4369-a0c7-7c2b21693d9b",
isSensitiveDebugLoggingEnabled = { false },
)
val storage = mock<Storage> {
on { contains("${account.uuid}.notifyNewMail") } doReturn true
on { getBoolean("${account.uuid}.notifyNewMail", false) } doReturn false
on { getBoolean("${account.uuid}.notifySelfNewMail", false) } doReturn false
}
val testSubject = DefaultAccountDefaultsProvider(
resourceProvider = resourceProvider,
featureFlagProvider = {
FeatureFlagResult.Enabled
},
)
// act
testSubject.applyOverwrites(account, storage)
// assert
assertThat(account.isNotifyNewMail).isFalse()
assertThat(account.isNotifySelfNewMail).isFalse()
}
@Suppress("MaxLineLength")
@Test
fun `applyOverwrites updates account notification values from featureFlag values when storage does not contain isNotifyNewMail value`() {
// arrange
val resourceProvider = mock<CoreResourceProvider> {
on { defaultIdentityDescription() } doReturn "Default Identity"
}
val account = LegacyAccount(
uuid = "cf728064-077d-4369-a0c7-7c2b21693d9b",
isSensitiveDebugLoggingEnabled = { false },
)
val storage = mock<Storage> {
on { contains("${account.uuid}.notifyNewMail") } doReturn false
on { getBoolean("${account.uuid}.notifyNewMail", false) } doReturn false
on { getBoolean("${account.uuid}.notifySelfNewMail", false) } doReturn false
}
val testSubject = DefaultAccountDefaultsProvider(
resourceProvider = resourceProvider,
featureFlagProvider = {
FeatureFlagResult.Enabled
},
)
// act
testSubject.applyOverwrites(account, storage)
// assert
assertThat(account.isNotifyNewMail).isTrue()
assertThat(account.isNotifySelfNewMail).isTrue()
}
}

View file

@ -0,0 +1,158 @@
package net.thunderbird.app.common.account.data
import app.cash.turbine.test
import assertk.assertThat
import assertk.assertions.isEqualTo
import com.fsck.k9.mail.AuthType
import com.fsck.k9.mail.ConnectionSecurity
import com.fsck.k9.mail.ServerSettings
import kotlinx.coroutines.test.runTest
import net.thunderbird.account.fake.FakeAccountProfileData.PROFILE_COLOR
import net.thunderbird.account.fake.FakeAccountProfileData.PROFILE_NAME
import net.thunderbird.core.android.account.Identity
import net.thunderbird.core.android.account.LegacyAccountWrapper
import net.thunderbird.feature.account.AccountId
import net.thunderbird.feature.account.AccountIdFactory
import net.thunderbird.feature.account.profile.AccountAvatar
import net.thunderbird.feature.account.profile.AccountProfile
import net.thunderbird.feature.account.storage.legacy.mapper.DefaultAccountAvatarDataMapper
import net.thunderbird.feature.account.storage.legacy.mapper.DefaultAccountProfileDataMapper
import net.thunderbird.feature.account.storage.profile.AvatarDto
import net.thunderbird.feature.account.storage.profile.AvatarTypeDto
import net.thunderbird.feature.account.storage.profile.ProfileDto
import org.junit.Test
class DefaultAccountProfileLocalDataSourceTest {
@Test
fun `getById should return account profile`() = runTest {
// arrange
val accountId = AccountIdFactory.create()
val legacyAccount = createLegacyAccount(accountId)
val accountProfile = createAccountProfile(accountId)
val testSubject = createTestSubject(legacyAccount)
// act & assert
testSubject.getById(accountId).test {
assertThat(awaitItem()).isEqualTo(accountProfile)
}
}
@Test
fun `getById should return null when account is not found`() = runTest {
// arrange
val accountId = AccountIdFactory.create()
val testSubject = createTestSubject(null)
// act & assert
testSubject.getById(accountId).test {
assertThat(awaitItem()).isEqualTo(null)
}
}
@Test
fun `update should save account profile`() = runTest {
// arrange
val accountId = AccountIdFactory.create()
val legacyAccount = createLegacyAccount(accountId)
val accountProfile = createAccountProfile(accountId)
val updatedName = "updatedName"
val updatedAccountProfile = accountProfile.copy(name = updatedName)
val testSubject = createTestSubject(legacyAccount)
// act & assert
testSubject.getById(accountId).test {
assertThat(awaitItem()).isEqualTo(accountProfile)
testSubject.update(updatedAccountProfile)
assertThat(awaitItem()).isEqualTo(updatedAccountProfile)
}
}
private companion object Companion {
fun createLegacyAccount(
id: AccountId,
displayName: String = PROFILE_NAME,
color: Int = PROFILE_COLOR,
): LegacyAccountWrapper {
return LegacyAccountWrapper(
isSensitiveDebugLoggingEnabled = { true },
id = id,
name = displayName,
email = "demo@example.com",
profile = ProfileDto(
id = id,
name = displayName,
color = color,
avatar = AvatarDto(
avatarType = AvatarTypeDto.ICON,
avatarMonogram = null,
avatarImageUri = null,
avatarIconName = "star",
),
),
identities = listOf(
Identity(
signatureUse = false,
description = "Demo User",
),
),
incomingServerSettings = ServerSettings(
type = "imap",
host = "imap.example.com",
port = 993,
connectionSecurity = ConnectionSecurity.SSL_TLS_REQUIRED,
authenticationType = AuthType.PLAIN,
username = "test",
password = "password",
clientCertificateAlias = null,
),
outgoingServerSettings = ServerSettings(
type = "smtp",
host = "smtp.example.com",
port = 465,
connectionSecurity = ConnectionSecurity.SSL_TLS_REQUIRED,
authenticationType = AuthType.PLAIN,
username = "test",
password = "password",
clientCertificateAlias = null,
),
)
}
private fun createAccountProfile(
accountId: AccountId,
name: String = PROFILE_NAME,
color: Int = PROFILE_COLOR,
): AccountProfile {
return AccountProfile(
id = accountId,
name = name,
color = color,
avatar = AccountAvatar.Icon(
name = "star",
),
)
}
private fun createTestSubject(
legacyAccount: LegacyAccountWrapper?,
): DefaultAccountProfileLocalDataSource {
return DefaultAccountProfileLocalDataSource(
accountManager = FakeLegacyAccountWrapperManager(
initialAccounts = if (legacyAccount != null) {
listOf(legacyAccount)
} else {
emptyList()
},
),
dataMapper = DefaultAccountProfileDataMapper(
avatarMapper = DefaultAccountAvatarDataMapper(),
),
)
}
}
}

View file

@ -0,0 +1,36 @@
package net.thunderbird.app.common.account.data
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.update
import net.thunderbird.core.android.account.LegacyAccountWrapper
import net.thunderbird.core.android.account.LegacyAccountWrapperManager
import net.thunderbird.feature.account.AccountId
internal class FakeLegacyAccountWrapperManager(
initialAccounts: List<LegacyAccountWrapper> = emptyList(),
) : LegacyAccountWrapperManager {
private val accountsState = MutableStateFlow(
initialAccounts,
)
private val accounts: StateFlow<List<LegacyAccountWrapper>> = accountsState
override fun getAll(): Flow<List<LegacyAccountWrapper>> = accounts
override fun getById(id: AccountId): Flow<LegacyAccountWrapper?> = accounts
.map { list ->
list.find { it.id == id }
}
override suspend fun update(account: LegacyAccountWrapper) {
accountsState.update { currentList ->
currentList.toMutableList().apply {
removeIf { it.uuid == account.uuid }
add(account)
}
}
}
}

12
app-k9mail/README.md Normal file
View file

@ -0,0 +1,12 @@
# K-9 Mail
This is the source code repository for the K-9 Mail project.
## Maintenance
### F-Droid
K-9 Mail is available on F-Droid. The apps metadata for F-Droid is available within the [metadata](fastlane/metadata)
folder. The metadata is setup according to
the [All About Descriptions, Graphics, and Screenshots](https://f-droid.org/en/docs/All_About_Descriptions_Graphics_and_Screenshots/)
and [Build Metadata Reference](https://f-droid.org/en/docs/Build_Metadata_Reference/).

View file

@ -0,0 +1,99 @@
application-icon-120:'res/drawable-v26/ic_launcher.xml'
application-icon-160:'res/drawable-v26/ic_launcher.xml'
application-icon-240:'res/drawable-v26/ic_launcher.xml'
application-icon-320:'res/drawable-v26/ic_launcher.xml'
application-icon-480:'res/drawable-v26/ic_launcher.xml'
application-icon-640:'res/drawable-v26/ic_launcher.xml'
application-icon-65534:'res/drawable-v26/ic_launcher.xml'
application-label-ar:'بريد K-9'
application-label-be:'Пошта K-9'
application-label-bg:'K-9 Поща'
application-label-ca:'K-9 Mail'
application-label-co:'K-9 Mail'
application-label-cs:'K-9 Mail'
application-label-cy:'K-9 Mail'
application-label-da:'K-9 Mail'
application-label-de:'K-9 Mail'
application-label-el:'K-9 Mail'
application-label-en-GB:'K-9 Mail'
application-label-en:'K-9 Mail'
application-label-eo:'K-9 Retpoŝtilo'
application-label-es:'K-9 Mail'
application-label-et:'K-9 Mail'
application-label-eu:'K-9 Mail'
application-label-fa:'نامهٔ کی۹'
application-label-fi:'K-9 Mail'
application-label-fr:'Courriel K-9'
application-label-fy:'K-9 Mail'
application-label-ga:'K-9 Post'
application-label-gl:'K-9 Mail'
application-label-hr:'K-9 Mail'
application-label-hu:'K-9 Mail'
application-label-in:'Surel K-9'
application-label-is:'K-9 - Póstur'
application-label-it:'K-9 Mail'
application-label-iw:'K-9 דוא\"ל'
application-label-ja:'K-9 Mail'
application-label-ko:'K-9 메일'
application-label-lt:'K-9 paštas'
application-label-lv:'K-9 pasts'
application-label-nb:'K-9 E-post'
application-label-nl:'K-9 Mail'
application-label-nn:'K-9 e-post'
application-label-pl:'K-9 Mail'
application-label-pt-BR:'K-9 Mail'
application-label-pt-PT:'K-9 Mail'
application-label-pt:'Email K-9'
application-label-ro:'K-9 Mail'
application-label-ru:'Почта K-9'
application-label-sk:'K-9 Mail'
application-label-sl:'Pošta K-9'
application-label-sq:'K-9 Mail'
application-label-sr:'K-9 Mail'
application-label-sv:'K-9 Mail'
application-label-tr:'K-9 Posta'
application-label-uk:'K-9 Mail'
application-label-vi:'Thư K-9'
application-label-zh-CN:'K-9 Mail'
application-label-zh-TW:'K-9 Mail'
application-label-zh:'K-9 Mail'
application-label:'K-9 Mail'
application: label='K-9 Mail' icon='res/drawable-v26/ic_launcher.xml'
densities: '120' '160' '240' '320' '480' '640' '65534'
feature-group: label=''
install-location:'auto'
launchable-activity: name='com.fsck.k9.activity.MessageList' label='' icon=''
locales: '--_--' 'ar' 'be' 'bg' 'ca' 'co' 'cs' 'cy' 'da' 'de' 'el' 'en' 'en-GB' 'eo' 'es' 'et' 'eu' 'fa' 'fi' 'fr' 'fy' 'ga' 'gl' 'hr' 'hu' 'in' 'is' 'it' 'iw' 'ja' 'ko' 'lt' 'lv' 'nb' 'nl' 'nn' 'pl' 'pt' 'pt-BR' 'pt-PT' 'ro' 'ru' 'sk' 'sl' 'sq' 'sr' 'sv' 'tr' 'uk' 'vi' 'zh' 'zh-CN' 'zh-TW'
main
minSdkVersion:'21'
native-code: 'arm64-v8a' 'armeabi-v7a' 'x86' 'x86_64'
other-activities
other-receivers
other-services
package: name='com.fsck.k9' platformBuildVersionName='15' platformBuildVersionCode='35' compileSdkVersion='35' compileSdkVersionCodename='15'
property: name='android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE' value='This service is used to maintain a continuous connection to an IMAP server to be able to provide instant notifications to the user when a new email arrives. Firebase Cloud Messaging is not suitable for this task, neither are mechanisms like AndroidX WorkManager. Other foreground service types aren't a good fit for this use case.'
provides-component:'app-widget'
supports-any-density: 'true'
supports-screens: 'small' 'normal' 'large' 'xlarge'
targetSdkVersion:'35'
uses-feature-not-required: name='android.hardware.camera'
uses-feature-not-required: name='android.hardware.touchscreen'
uses-library-not-required:'androidx.window.extensions'
uses-library-not-required:'androidx.window.sidecar'
uses-library-not-required:'com.sec.android.app.multiwindow'
uses-permission: name='android.permission.ACCESS_NETWORK_STATE'
uses-permission: name='android.permission.CAMERA'
uses-permission: name='android.permission.FOREGROUND_SERVICE'
uses-permission: name='android.permission.FOREGROUND_SERVICE_DATA_SYNC' maxSdkVersion='33'
uses-permission: name='android.permission.FOREGROUND_SERVICE_SPECIAL_USE'
uses-permission: name='android.permission.INTERNET'
uses-permission: name='android.permission.POST_NOTIFICATIONS'
uses-permission: name='android.permission.READ_CONTACTS'
uses-permission: name='android.permission.READ_SYNC_SETTINGS'
uses-permission: name='android.permission.RECEIVE_BOOT_COMPLETED'
uses-permission: name='android.permission.SCHEDULE_EXACT_ALARM'
uses-permission: name='android.permission.USE_BIOMETRIC'
uses-permission: name='android.permission.USE_FINGERPRINT'
uses-permission: name='android.permission.VIBRATE'
uses-permission: name='android.permission.WAKE_LOCK'
uses-permission: name='com.fsck.k9.DYNAMIC_RECEIVER_NOT_EXPORTED_PERMISSION'

View file

@ -0,0 +1,100 @@
application-icon-120:'res/drawable-v26/ic_launcher.xml'
application-icon-160:'res/drawable-v26/ic_launcher.xml'
application-icon-240:'res/drawable-v26/ic_launcher.xml'
application-icon-320:'res/drawable-v26/ic_launcher.xml'
application-icon-480:'res/drawable-v26/ic_launcher.xml'
application-icon-640:'res/drawable-v26/ic_launcher.xml'
application-icon-65534:'res/drawable-v26/ic_launcher.xml'
application-label-ar:'بريد K-9'
application-label-be:'Пошта K-9'
application-label-bg:'K-9 Поща'
application-label-ca:'K-9 Mail'
application-label-co:'K-9 Mail'
application-label-cs:'K-9 Mail'
application-label-cy:'K-9 Mail'
application-label-da:'K-9 Mail'
application-label-de:'K-9 Mail'
application-label-el:'K-9 Mail'
application-label-en-GB:'K-9 Mail'
application-label-en:'K-9 Mail'
application-label-eo:'K-9 Retpoŝtilo'
application-label-es:'K-9 Mail'
application-label-et:'K-9 Mail'
application-label-eu:'K-9 Mail'
application-label-fa:'نامهٔ کی۹'
application-label-fi:'K-9 Mail'
application-label-fr:'Courriel K-9'
application-label-fy:'K-9 Mail'
application-label-ga:'K-9 Post'
application-label-gl:'K-9 Mail'
application-label-hr:'K-9 Mail'
application-label-hu:'K-9 Mail'
application-label-in:'Surel K-9'
application-label-is:'K-9 - Póstur'
application-label-it:'K-9 Mail'
application-label-iw:'K-9 דוא\"ל'
application-label-ja:'K-9 Mail'
application-label-ko:'K-9 메일'
application-label-lt:'K-9 paštas'
application-label-lv:'K-9 pasts'
application-label-nb:'K-9 E-post'
application-label-nl:'K-9 Mail'
application-label-nn:'K-9 e-post'
application-label-pl:'K-9 Mail'
application-label-pt-BR:'K-9 Mail'
application-label-pt-PT:'K-9 Mail'
application-label-pt:'Email K-9'
application-label-ro:'K-9 Mail'
application-label-ru:'Почта K-9'
application-label-sk:'K-9 Mail'
application-label-sl:'Pošta K-9'
application-label-sq:'K-9 Mail'
application-label-sr:'K-9 Mail'
application-label-sv:'K-9 Mail'
application-label-tr:'K-9 Posta'
application-label-uk:'K-9 Mail'
application-label-vi:'Thư K-9'
application-label-zh-CN:'K-9 Mail'
application-label-zh-TW:'K-9 Mail'
application-label-zh:'K-9 Mail'
application-label:'K-9 Mail'
application: label='K-9 Mail' icon='res/drawable-v26/ic_launcher.xml'
densities: '120' '160' '240' '320' '480' '640' '65534'
feature-group: label=''
install-location:'auto'
launchable-activity: name='com.fsck.k9.activity.MessageList' label='' icon=''
locales: '--_--' 'ar' 'be' 'bg' 'ca' 'co' 'cs' 'cy' 'da' 'de' 'el' 'en' 'en-GB' 'eo' 'es' 'et' 'eu' 'fa' 'fi' 'fr' 'fy' 'ga' 'gl' 'hr' 'hu' 'in' 'is' 'it' 'iw' 'ja' 'ko' 'lt' 'lv' 'nb' 'nl' 'nn' 'pl' 'pt' 'pt-BR' 'pt-PT' 'ro' 'ru' 'sk' 'sl' 'sq' 'sr' 'sv' 'tr' 'uk' 'vi' 'zh' 'zh-CN' 'zh-TW'
main
minSdkVersion:'21'
native-code: 'arm64-v8a' 'armeabi-v7a' 'x86' 'x86_64'
other-activities
other-receivers
other-services
package: name='com.fsck.k9' platformBuildVersionName='15' platformBuildVersionCode='35' compileSdkVersion='35' compileSdkVersionCodename='15'
property: name='android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE' value='This service is used to maintain a continuous connection to an IMAP server to be able to provide instant notifications to the user when a new email arrives. Firebase Cloud Messaging is not suitable for this task, neither are mechanisms like AndroidX WorkManager. Other foreground service types aren't a good fit for this use case.'
provides-component:'app-widget'
supports-any-density: 'true'
supports-screens: 'small' 'normal' 'large' 'xlarge'
targetSdkVersion:'35'
uses-feature-not-required: name='android.hardware.camera'
uses-feature-not-required: name='android.hardware.touchscreen'
uses-library-not-required:'androidx.window.extensions'
uses-library-not-required:'androidx.window.sidecar'
uses-library-not-required:'com.sec.android.app.multiwindow'
uses-permission: name='android.permission.ACCESS_NETWORK_STATE'
uses-permission: name='android.permission.CAMERA'
uses-permission: name='android.permission.FOREGROUND_SERVICE'
uses-permission: name='android.permission.FOREGROUND_SERVICE_DATA_SYNC' maxSdkVersion='33'
uses-permission: name='android.permission.FOREGROUND_SERVICE_SPECIAL_USE'
uses-permission: name='android.permission.INTERNET'
uses-permission: name='android.permission.POST_NOTIFICATIONS'
uses-permission: name='android.permission.READ_CONTACTS'
uses-permission: name='android.permission.READ_SYNC_SETTINGS'
uses-permission: name='android.permission.RECEIVE_BOOT_COMPLETED'
uses-permission: name='android.permission.SCHEDULE_EXACT_ALARM'
uses-permission: name='android.permission.USE_BIOMETRIC'
uses-permission: name='android.permission.USE_FINGERPRINT'
uses-permission: name='android.permission.VIBRATE'
uses-permission: name='android.permission.WAKE_LOCK'
uses-permission: name='com.android.vending.BILLING'
uses-permission: name='com.fsck.k9.DYNAMIC_RECEIVER_NOT_EXPORTED_PERMISSION'

175
app-k9mail/build.gradle.kts Normal file
View file

@ -0,0 +1,175 @@
plugins {
id(ThunderbirdPlugins.App.androidCompose)
alias(libs.plugins.dependency.guard)
id("thunderbird.app.version.info")
id("thunderbird.quality.badging")
}
val testCoverageEnabled: Boolean by extra
if (testCoverageEnabled) {
apply(plugin = "jacoco")
}
android {
namespace = "com.fsck.k9"
defaultConfig {
applicationId = "com.fsck.k9"
testApplicationId = "com.fsck.k9.tests"
versionCode = 39029
versionName = "13.0"
buildConfigField("String", "CLIENT_INFO_APP_NAME", "\"K-9 Mail\"")
}
androidResources {
// Keep in sync with the resource string array "supported_languages"
localeFilters += listOf(
"ar",
"be",
"bg",
"ca",
"co",
"cs",
"cy",
"da",
"de",
"el",
"en",
"en-rGB",
"eo",
"es",
"et",
"eu",
"fa",
"fi",
"fr",
"fy",
"ga",
"gl",
"hr",
"hu",
"in",
"is",
"it",
"iw",
"ja",
"ko",
"lt",
"lv",
"nb",
"nl",
"nn",
"pl",
"pt-rBR",
"pt-rPT",
"ro",
"ru",
"sk",
"sl",
"sq",
"sr",
"sv",
"tr",
"uk",
"vi",
"zh-rCN",
"zh-rTW",
)
}
signingConfigs {
createSigningConfig(project, SigningType.K9_RELEASE, isUpload = false)
}
buildTypes {
release {
signingConfig = signingConfigs.getByType(SigningType.K9_RELEASE)
isMinifyEnabled = true
proguardFiles(
getDefaultProguardFile("proguard-android.txt"),
"proguard-rules.pro",
)
}
debug {
applicationIdSuffix = ".debug"
enableUnitTestCoverage = testCoverageEnabled
enableAndroidTestCoverage = testCoverageEnabled
isMinifyEnabled = false
}
}
flavorDimensions += listOf("app")
productFlavors {
create("foss") {
dimension = "app"
buildConfigField("String", "PRODUCT_FLAVOR_APP", "\"foss\"")
}
create("full") {
dimension = "app"
buildConfigField("String", "PRODUCT_FLAVOR_APP", "\"full\"")
}
}
packaging {
jniLibs {
excludes += listOf("kotlin/**")
}
resources {
excludes += listOf(
"META-INF/*.kotlin_module",
"META-INF/*.version",
"kotlin/**",
"DebugProbesKt.bin",
)
}
}
}
dependencies {
implementation(projects.appCommon)
implementation(projects.core.ui.compose.theme2.k9mail)
implementation(projects.core.ui.legacy.theme2.k9mail)
implementation(projects.feature.launcher)
implementation(projects.feature.mail.message.list)
implementation(projects.legacy.core)
implementation(projects.legacy.ui.legacy)
implementation(projects.core.featureflag)
implementation(projects.feature.account.settings.impl)
"fossImplementation"(projects.feature.funding.noop)
"fullImplementation"(projects.feature.funding.googleplay)
implementation(projects.feature.migration.launcher.noop)
implementation(projects.feature.onboarding.migration.noop)
implementation(projects.feature.telemetry.noop)
implementation(projects.feature.widget.messageList)
implementation(projects.feature.widget.messageListGlance)
implementation(projects.feature.widget.shortcut)
implementation(projects.feature.widget.unread)
implementation(libs.androidx.work.runtime)
implementation(projects.feature.autodiscovery.api)
debugImplementation(projects.backend.demo)
debugImplementation(projects.feature.autodiscovery.demo)
// Required for DependencyInjectionTest
testImplementation(projects.feature.account.api)
testImplementation(projects.feature.account.common)
testImplementation(projects.plugins.openpgpApiLib.openpgpApi)
testImplementation(libs.appauth)
}
dependencyGuard {
configuration("fossReleaseRuntimeClasspath")
configuration("fullReleaseRuntimeClasspath")
}

View file

@ -0,0 +1,286 @@
androidx.activity:activity-compose:1.10.1
androidx.activity:activity-ktx:1.10.1
androidx.activity:activity:1.10.1
androidx.annotation:annotation-experimental:1.4.1
androidx.annotation:annotation-jvm:1.9.1
androidx.annotation:annotation:1.9.1
androidx.appcompat:appcompat-resources:1.7.1
androidx.appcompat:appcompat:1.7.1
androidx.arch.core:core-common:2.2.0
androidx.arch.core:core-runtime:2.2.0
androidx.autofill:autofill:1.3.0
androidx.biometric:biometric:1.1.0
androidx.browser:browser:1.3.0
androidx.cardview:cardview:1.0.0
androidx.collection:collection-jvm:1.5.0
androidx.collection:collection-ktx:1.5.0
androidx.collection:collection:1.5.0
androidx.compose.animation:animation-android:1.8.3
androidx.compose.animation:animation-core-android:1.8.3
androidx.compose.animation:animation-core:1.8.3
androidx.compose.animation:animation:1.8.3
androidx.compose.foundation:foundation-android:1.8.3
androidx.compose.foundation:foundation-layout-android:1.8.3
androidx.compose.foundation:foundation-layout:1.8.3
androidx.compose.foundation:foundation:1.8.3
androidx.compose.material3.adaptive:adaptive-android:1.1.0
androidx.compose.material3.adaptive:adaptive-layout-android:1.1.0
androidx.compose.material3.adaptive:adaptive-layout:1.1.0
androidx.compose.material3.adaptive:adaptive-navigation-android:1.1.0
androidx.compose.material3.adaptive:adaptive-navigation:1.1.0
androidx.compose.material3.adaptive:adaptive:1.1.0
androidx.compose.material3:material3-android:1.3.2
androidx.compose.material3:material3:1.3.2
androidx.compose.material:material-icons-core-android:1.7.8
androidx.compose.material:material-icons-core:1.7.8
androidx.compose.material:material-icons-extended-android:1.7.8
androidx.compose.material:material-icons-extended:1.7.8
androidx.compose.material:material-ripple-android:1.8.3
androidx.compose.material:material-ripple:1.8.3
androidx.compose.runtime:runtime-android:1.8.3
androidx.compose.runtime:runtime-saveable-android:1.8.3
androidx.compose.runtime:runtime-saveable:1.8.3
androidx.compose.runtime:runtime:1.8.3
androidx.compose.ui:ui-android:1.8.3
androidx.compose.ui:ui-geometry-android:1.8.3
androidx.compose.ui:ui-geometry:1.8.3
androidx.compose.ui:ui-graphics-android:1.8.3
androidx.compose.ui:ui-graphics:1.8.3
androidx.compose.ui:ui-text-android:1.8.3
androidx.compose.ui:ui-text:1.8.3
androidx.compose.ui:ui-tooling-preview-android:1.8.3
androidx.compose.ui:ui-tooling-preview:1.8.3
androidx.compose.ui:ui-unit-android:1.8.3
androidx.compose.ui:ui-unit:1.8.3
androidx.compose.ui:ui-util-android:1.8.3
androidx.compose.ui:ui-util:1.8.3
androidx.compose.ui:ui:1.8.3
androidx.compose:compose-bom:2025.07.00
androidx.concurrent:concurrent-futures-ktx:1.1.0
androidx.concurrent:concurrent-futures:1.1.0
androidx.constraintlayout:constraintlayout-core:1.1.1
androidx.constraintlayout:constraintlayout:2.2.1
androidx.coordinatorlayout:coordinatorlayout:1.3.0
androidx.core:core-ktx:1.16.0
androidx.core:core-remoteviews:1.1.0
androidx.core:core-splashscreen:1.0.1
androidx.core:core-viewtree:1.0.0
androidx.core:core:1.16.0
androidx.cursoradapter:cursoradapter:1.0.0
androidx.customview:customview-poolingcontainer:1.0.0
androidx.customview:customview:1.1.0
androidx.datastore:datastore-core:1.0.0
androidx.datastore:datastore-preferences-core:1.0.0
androidx.datastore:datastore-preferences:1.0.0
androidx.datastore:datastore:1.0.0
androidx.documentfile:documentfile:1.0.0
androidx.drawerlayout:drawerlayout:1.1.1
androidx.dynamicanimation:dynamicanimation:1.0.0
androidx.emoji2:emoji2-views-helper:1.4.0
androidx.emoji2:emoji2:1.4.0
androidx.exifinterface:exifinterface:1.4.1
androidx.fragment:fragment-compose:1.8.8
androidx.fragment:fragment-ktx:1.8.8
androidx.fragment:fragment:1.8.8
androidx.glance:glance-appwidget-external-protobuf:1.1.1
androidx.glance:glance-appwidget-proto:1.1.1
androidx.glance:glance-appwidget:1.1.1
androidx.glance:glance-material3:1.1.1
androidx.glance:glance:1.1.1
androidx.graphics:graphics-path:1.0.1
androidx.interpolator:interpolator:1.0.0
androidx.legacy:legacy-support-core-utils:1.0.0
androidx.lifecycle:lifecycle-common-java8:2.9.2
androidx.lifecycle:lifecycle-common-jvm:2.9.2
androidx.lifecycle:lifecycle-common:2.9.2
androidx.lifecycle:lifecycle-livedata-core-ktx:2.9.2
androidx.lifecycle:lifecycle-livedata-core:2.9.2
androidx.lifecycle:lifecycle-livedata-ktx:2.9.2
androidx.lifecycle:lifecycle-livedata:2.9.2
androidx.lifecycle:lifecycle-process:2.9.2
androidx.lifecycle:lifecycle-runtime-android:2.9.2
androidx.lifecycle:lifecycle-runtime-compose-android:2.9.2
androidx.lifecycle:lifecycle-runtime-compose:2.9.2
androidx.lifecycle:lifecycle-runtime-ktx-android:2.9.2
androidx.lifecycle:lifecycle-runtime-ktx:2.9.2
androidx.lifecycle:lifecycle-runtime:2.9.2
androidx.lifecycle:lifecycle-service:2.9.2
androidx.lifecycle:lifecycle-viewmodel-android:2.9.2
androidx.lifecycle:lifecycle-viewmodel-compose-android:2.9.2
androidx.lifecycle:lifecycle-viewmodel-compose:2.9.2
androidx.lifecycle:lifecycle-viewmodel-ktx:2.9.2
androidx.lifecycle:lifecycle-viewmodel-savedstate-android:2.9.2
androidx.lifecycle:lifecycle-viewmodel-savedstate:2.9.2
androidx.lifecycle:lifecycle-viewmodel:2.9.2
androidx.loader:loader:1.0.0
androidx.localbroadcastmanager:localbroadcastmanager:1.1.0
androidx.navigation:navigation-common-android:2.9.3
androidx.navigation:navigation-common:2.9.3
androidx.navigation:navigation-compose-android:2.9.3
androidx.navigation:navigation-compose:2.9.3
androidx.navigation:navigation-fragment:2.9.3
androidx.navigation:navigation-runtime-android:2.9.3
androidx.navigation:navigation-runtime:2.9.3
androidx.navigation:navigation-ui:2.9.3
androidx.preference:preference:1.2.1
androidx.print:print:1.0.0
androidx.profileinstaller:profileinstaller:1.4.1
androidx.recyclerview:recyclerview:1.4.0
androidx.resourceinspection:resourceinspection-annotation:1.0.1
androidx.room:room-common:2.6.1
androidx.room:room-ktx:2.6.1
androidx.room:room-runtime:2.6.1
androidx.savedstate:savedstate-android:1.3.1
androidx.savedstate:savedstate-compose-android:1.3.1
androidx.savedstate:savedstate-compose:1.3.1
androidx.savedstate:savedstate-ktx:1.3.1
androidx.savedstate:savedstate:1.3.1
androidx.slidingpanelayout:slidingpanelayout:1.2.0
androidx.sqlite:sqlite-framework:2.4.0
androidx.sqlite:sqlite:2.4.0
androidx.startup:startup-runtime:1.1.1
androidx.swiperefreshlayout:swiperefreshlayout:1.1.0
androidx.tracing:tracing-ktx:1.2.0
androidx.tracing:tracing:1.2.0
androidx.transition:transition:1.5.0
androidx.vectordrawable:vectordrawable-animated:1.2.0
androidx.vectordrawable:vectordrawable:1.2.0
androidx.versionedparcelable:versionedparcelable:1.1.1
androidx.viewpager2:viewpager2:1.1.0-beta02
androidx.viewpager:viewpager:1.0.0
androidx.webkit:webkit:1.14.0
androidx.window.extensions.core:core:1.0.0
androidx.window:window-core-android:1.3.0
androidx.window:window-core:1.3.0
androidx.window:window:1.3.0
androidx.work:work-runtime-ktx:2.10.3
androidx.work:work-runtime:2.10.3
co.touchlab:stately-concurrency-jvm:2.1.0
co.touchlab:stately-concurrency:2.1.0
co.touchlab:stately-concurrent-collections-jvm:2.1.0
co.touchlab:stately-concurrent-collections:2.1.0
co.touchlab:stately-strict-jvm:2.1.0
co.touchlab:stately-strict:2.1.0
com.beetstra.jutf7:jutf7:1.0.0
com.github.ByteHamster:SearchPreference:2.7.3
com.github.bumptech.glide:annotations:4.16.0
com.github.bumptech.glide:disklrucache:4.16.0
com.github.bumptech.glide:gifdecoder:4.16.0
com.github.bumptech.glide:glide:4.16.0
com.github.skydoves:landscapist-android:2.5.1
com.github.skydoves:landscapist-coil3-android:2.5.1
com.github.skydoves:landscapist-coil3:2.5.1
com.github.skydoves:landscapist:2.5.1
com.google.android.flexbox:flexbox:3.0.0
com.google.android.material:material:1.12.0
com.google.errorprone:error_prone_annotations:2.15.0
com.google.guava:listenablefuture:1.0
com.jakewharton.timber:timber:5.0.1
com.jcraft:jzlib:1.0.7
com.mikepenz:fastadapter-extensions-drag:5.7.0
com.mikepenz:fastadapter-extensions-expandable:5.7.0
com.mikepenz:fastadapter-extensions-swipe:5.7.0
com.mikepenz:fastadapter-extensions-utils:5.7.0
com.mikepenz:fastadapter:5.7.0
com.squareup.moshi:moshi:1.15.2
com.squareup.okhttp3:okhttp:4.12.0
com.squareup.okio:okio-jvm:3.16.0
com.squareup.okio:okio:3.16.0
com.takisoft.colorpicker:colorpicker:1.0.0
com.takisoft.datetimepicker:datetimepicker:1.0.2
com.takisoft.preferencex:preferencex-colorpicker:1.1.0
com.takisoft.preferencex:preferencex-datetimepicker:1.1.0
com.takisoft.preferencex:preferencex:1.1.0
commons-io:commons-io:2.20.0
de.cketti.library.changelog:ckchangelog-core:2.0.0-beta02
de.cketti.safecontentresolver:safe-content-resolver-v21:1.0.0
de.hdodenhof:circleimageview:3.1.0
io.coil-kt.coil3:coil-android:3.2.0
io.coil-kt.coil3:coil-core-android:3.2.0
io.coil-kt.coil3:coil-core:3.2.0
io.coil-kt.coil3:coil-gif:3.2.0
io.coil-kt.coil3:coil-network-core-android:3.2.0
io.coil-kt.coil3:coil-network-core:3.2.0
io.coil-kt.coil3:coil-network-okhttp-jvm:3.2.0
io.coil-kt.coil3:coil-network-okhttp:3.2.0
io.coil-kt.coil3:coil-video:3.2.0
io.coil-kt.coil3:coil:3.2.0
io.insert-koin:koin-android:4.1.0
io.insert-koin:koin-androidx-compose:4.1.0
io.insert-koin:koin-bom:4.1.0
io.insert-koin:koin-compose-android:4.1.0
io.insert-koin:koin-compose-viewmodel-android:4.1.0
io.insert-koin:koin-compose-viewmodel:4.1.0
io.insert-koin:koin-compose:4.1.0
io.insert-koin:koin-core-jvm:4.1.0
io.insert-koin:koin-core-viewmodel-android:4.1.0
io.insert-koin:koin-core-viewmodel:4.1.0
io.insert-koin:koin-core:4.1.0
net.jcip:jcip-annotations:1.0
net.openid:appauth:0.11.1
org.apache.commons:commons-lang3:3.7
org.apache.commons:commons-text:1.3
org.apache.httpcomponents.client5:httpclient5:5.5
org.apache.httpcomponents.core5:httpcore5-h2:5.3.4
org.apache.httpcomponents.core5:httpcore5:5.3.4
org.apache.james:apache-mime4j-core:0.8.13
org.apache.james:apache-mime4j-dom:0.8.13
org.jetbrains.androidx.lifecycle:lifecycle-common:2.9.1
org.jetbrains.androidx.lifecycle:lifecycle-runtime-compose:2.9.1
org.jetbrains.androidx.lifecycle:lifecycle-runtime:2.9.1
org.jetbrains.androidx.lifecycle:lifecycle-viewmodel-compose:2.9.1
org.jetbrains.androidx.lifecycle:lifecycle-viewmodel-savedstate:2.9.1
org.jetbrains.androidx.lifecycle:lifecycle-viewmodel:2.9.1
org.jetbrains.androidx.savedstate:savedstate:1.3.1
org.jetbrains.compose.animation:animation-core:1.8.2
org.jetbrains.compose.animation:animation:1.8.2
org.jetbrains.compose.annotation-internal:annotation:1.8.2
org.jetbrains.compose.collection-internal:collection:1.8.2
org.jetbrains.compose.components:components-resources-android:1.8.2
org.jetbrains.compose.components:components-resources:1.8.2
org.jetbrains.compose.components:components-ui-tooling-preview-android:1.8.2
org.jetbrains.compose.components:components-ui-tooling-preview:1.8.2
org.jetbrains.compose.foundation:foundation-layout:1.8.2
org.jetbrains.compose.foundation:foundation:1.8.2
org.jetbrains.compose.runtime:runtime-saveable:1.8.2
org.jetbrains.compose.runtime:runtime:1.8.2
org.jetbrains.compose.ui:ui-geometry:1.8.2
org.jetbrains.compose.ui:ui-graphics:1.8.2
org.jetbrains.compose.ui:ui-text:1.8.2
org.jetbrains.compose.ui:ui-tooling-preview:1.8.2
org.jetbrains.compose.ui:ui-unit:1.8.2
org.jetbrains.compose.ui:ui-util:1.8.2
org.jetbrains.compose.ui:ui:1.8.2
org.jetbrains.kotlin:kotlin-android-extensions-runtime:2.2.0
org.jetbrains.kotlin:kotlin-bom:2.2.0
org.jetbrains.kotlin:kotlin-parcelize-runtime:2.2.0
org.jetbrains.kotlin:kotlin-stdlib-common:2.2.0
org.jetbrains.kotlin:kotlin-stdlib-jdk7:2.2.0
org.jetbrains.kotlin:kotlin-stdlib-jdk8:2.2.0
org.jetbrains.kotlin:kotlin-stdlib:2.2.0
org.jetbrains.kotlinx:kotlinx-collections-immutable-jvm:0.4.0
org.jetbrains.kotlinx:kotlinx-collections-immutable:0.4.0
org.jetbrains.kotlinx:kotlinx-coroutines-android:1.10.2
org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.10.2
org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.10.2
org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.2
org.jetbrains.kotlinx:kotlinx-datetime-jvm:0.7.1
org.jetbrains.kotlinx:kotlinx-datetime:0.7.1
org.jetbrains.kotlinx:kotlinx-io-bytestring-jvm:0.8.0
org.jetbrains.kotlinx:kotlinx-io-bytestring:0.8.0
org.jetbrains.kotlinx:kotlinx-io-core-jvm:0.8.0
org.jetbrains.kotlinx:kotlinx-io-core:0.8.0
org.jetbrains.kotlinx:kotlinx-serialization-bom:1.9.0
org.jetbrains.kotlinx:kotlinx-serialization-core-jvm:1.9.0
org.jetbrains.kotlinx:kotlinx-serialization-core:1.9.0
org.jetbrains.kotlinx:kotlinx-serialization-json-jvm:1.9.0
org.jetbrains.kotlinx:kotlinx-serialization-json:1.9.0
org.jetbrains:annotations:26.0.2
org.jsoup:jsoup:1.19.1
org.jspecify:jspecify:1.0.0
org.minidns:minidns-client:1.1.1
org.minidns:minidns-core:1.1.1
org.minidns:minidns-dnssec:1.1.1
org.minidns:minidns-hla:1.1.1
org.minidns:minidns-iterative-resolver:1.1.1
org.slf4j:slf4j-api:1.7.36

View file

@ -0,0 +1,300 @@
androidx.activity:activity-compose:1.10.1
androidx.activity:activity-ktx:1.10.1
androidx.activity:activity:1.10.1
androidx.annotation:annotation-experimental:1.4.1
androidx.annotation:annotation-jvm:1.9.1
androidx.annotation:annotation:1.9.1
androidx.appcompat:appcompat-resources:1.7.1
androidx.appcompat:appcompat:1.7.1
androidx.arch.core:core-common:2.2.0
androidx.arch.core:core-runtime:2.2.0
androidx.autofill:autofill:1.3.0
androidx.biometric:biometric:1.1.0
androidx.browser:browser:1.3.0
androidx.cardview:cardview:1.0.0
androidx.collection:collection-jvm:1.5.0
androidx.collection:collection-ktx:1.5.0
androidx.collection:collection:1.5.0
androidx.compose.animation:animation-android:1.8.3
androidx.compose.animation:animation-core-android:1.8.3
androidx.compose.animation:animation-core:1.8.3
androidx.compose.animation:animation:1.8.3
androidx.compose.foundation:foundation-android:1.8.3
androidx.compose.foundation:foundation-layout-android:1.8.3
androidx.compose.foundation:foundation-layout:1.8.3
androidx.compose.foundation:foundation:1.8.3
androidx.compose.material3.adaptive:adaptive-android:1.1.0
androidx.compose.material3.adaptive:adaptive-layout-android:1.1.0
androidx.compose.material3.adaptive:adaptive-layout:1.1.0
androidx.compose.material3.adaptive:adaptive-navigation-android:1.1.0
androidx.compose.material3.adaptive:adaptive-navigation:1.1.0
androidx.compose.material3.adaptive:adaptive:1.1.0
androidx.compose.material3:material3-android:1.3.2
androidx.compose.material3:material3:1.3.2
androidx.compose.material:material-icons-core-android:1.7.8
androidx.compose.material:material-icons-core:1.7.8
androidx.compose.material:material-icons-extended-android:1.7.8
androidx.compose.material:material-icons-extended:1.7.8
androidx.compose.material:material-ripple-android:1.8.3
androidx.compose.material:material-ripple:1.8.3
androidx.compose.runtime:runtime-android:1.8.3
androidx.compose.runtime:runtime-saveable-android:1.8.3
androidx.compose.runtime:runtime-saveable:1.8.3
androidx.compose.runtime:runtime:1.8.3
androidx.compose.ui:ui-android:1.8.3
androidx.compose.ui:ui-geometry-android:1.8.3
androidx.compose.ui:ui-geometry:1.8.3
androidx.compose.ui:ui-graphics-android:1.8.3
androidx.compose.ui:ui-graphics:1.8.3
androidx.compose.ui:ui-text-android:1.8.3
androidx.compose.ui:ui-text:1.8.3
androidx.compose.ui:ui-tooling-preview-android:1.8.3
androidx.compose.ui:ui-tooling-preview:1.8.3
androidx.compose.ui:ui-unit-android:1.8.3
androidx.compose.ui:ui-unit:1.8.3
androidx.compose.ui:ui-util-android:1.8.3
androidx.compose.ui:ui-util:1.8.3
androidx.compose.ui:ui:1.8.3
androidx.compose:compose-bom:2025.07.00
androidx.concurrent:concurrent-futures-ktx:1.1.0
androidx.concurrent:concurrent-futures:1.1.0
androidx.constraintlayout:constraintlayout-core:1.1.1
androidx.constraintlayout:constraintlayout:2.2.1
androidx.coordinatorlayout:coordinatorlayout:1.3.0
androidx.core:core-ktx:1.16.0
androidx.core:core-remoteviews:1.1.0
androidx.core:core-splashscreen:1.0.1
androidx.core:core-viewtree:1.0.0
androidx.core:core:1.16.0
androidx.cursoradapter:cursoradapter:1.0.0
androidx.customview:customview-poolingcontainer:1.0.0
androidx.customview:customview:1.1.0
androidx.datastore:datastore-core:1.0.0
androidx.datastore:datastore-preferences-core:1.0.0
androidx.datastore:datastore-preferences:1.0.0
androidx.datastore:datastore:1.0.0
androidx.documentfile:documentfile:1.0.0
androidx.drawerlayout:drawerlayout:1.1.1
androidx.dynamicanimation:dynamicanimation:1.0.0
androidx.emoji2:emoji2-views-helper:1.4.0
androidx.emoji2:emoji2:1.4.0
androidx.exifinterface:exifinterface:1.4.1
androidx.fragment:fragment-compose:1.8.8
androidx.fragment:fragment-ktx:1.8.8
androidx.fragment:fragment:1.8.8
androidx.glance:glance-appwidget-external-protobuf:1.1.1
androidx.glance:glance-appwidget-proto:1.1.1
androidx.glance:glance-appwidget:1.1.1
androidx.glance:glance-material3:1.1.1
androidx.glance:glance:1.1.1
androidx.graphics:graphics-path:1.0.1
androidx.interpolator:interpolator:1.0.0
androidx.legacy:legacy-support-core-utils:1.0.0
androidx.lifecycle:lifecycle-common-java8:2.9.2
androidx.lifecycle:lifecycle-common-jvm:2.9.2
androidx.lifecycle:lifecycle-common:2.9.2
androidx.lifecycle:lifecycle-livedata-core-ktx:2.9.2
androidx.lifecycle:lifecycle-livedata-core:2.9.2
androidx.lifecycle:lifecycle-livedata-ktx:2.9.2
androidx.lifecycle:lifecycle-livedata:2.9.2
androidx.lifecycle:lifecycle-process:2.9.2
androidx.lifecycle:lifecycle-runtime-android:2.9.2
androidx.lifecycle:lifecycle-runtime-compose-android:2.9.2
androidx.lifecycle:lifecycle-runtime-compose:2.9.2
androidx.lifecycle:lifecycle-runtime-ktx-android:2.9.2
androidx.lifecycle:lifecycle-runtime-ktx:2.9.2
androidx.lifecycle:lifecycle-runtime:2.9.2
androidx.lifecycle:lifecycle-service:2.9.2
androidx.lifecycle:lifecycle-viewmodel-android:2.9.2
androidx.lifecycle:lifecycle-viewmodel-compose-android:2.9.2
androidx.lifecycle:lifecycle-viewmodel-compose:2.9.2
androidx.lifecycle:lifecycle-viewmodel-ktx:2.9.2
androidx.lifecycle:lifecycle-viewmodel-savedstate-android:2.9.2
androidx.lifecycle:lifecycle-viewmodel-savedstate:2.9.2
androidx.lifecycle:lifecycle-viewmodel:2.9.2
androidx.loader:loader:1.0.0
androidx.localbroadcastmanager:localbroadcastmanager:1.1.0
androidx.navigation:navigation-common-android:2.9.3
androidx.navigation:navigation-common:2.9.3
androidx.navigation:navigation-compose-android:2.9.3
androidx.navigation:navigation-compose:2.9.3
androidx.navigation:navigation-fragment:2.9.3
androidx.navigation:navigation-runtime-android:2.9.3
androidx.navigation:navigation-runtime:2.9.3
androidx.navigation:navigation-ui:2.9.3
androidx.preference:preference:1.2.1
androidx.print:print:1.0.0
androidx.profileinstaller:profileinstaller:1.4.1
androidx.recyclerview:recyclerview:1.4.0
androidx.resourceinspection:resourceinspection-annotation:1.0.1
androidx.room:room-common:2.6.1
androidx.room:room-ktx:2.6.1
androidx.room:room-runtime:2.6.1
androidx.savedstate:savedstate-android:1.3.1
androidx.savedstate:savedstate-compose-android:1.3.1
androidx.savedstate:savedstate-compose:1.3.1
androidx.savedstate:savedstate-ktx:1.3.1
androidx.savedstate:savedstate:1.3.1
androidx.slidingpanelayout:slidingpanelayout:1.2.0
androidx.sqlite:sqlite-framework:2.4.0
androidx.sqlite:sqlite:2.4.0
androidx.startup:startup-runtime:1.1.1
androidx.swiperefreshlayout:swiperefreshlayout:1.1.0
androidx.tracing:tracing-ktx:1.2.0
androidx.tracing:tracing:1.2.0
androidx.transition:transition:1.5.0
androidx.vectordrawable:vectordrawable-animated:1.2.0
androidx.vectordrawable:vectordrawable:1.2.0
androidx.versionedparcelable:versionedparcelable:1.1.1
androidx.viewpager2:viewpager2:1.1.0-beta02
androidx.viewpager:viewpager:1.0.0
androidx.webkit:webkit:1.14.0
androidx.window.extensions.core:core:1.0.0
androidx.window:window-core-android:1.3.0
androidx.window:window-core:1.3.0
androidx.window:window:1.3.0
androidx.work:work-runtime-ktx:2.10.3
androidx.work:work-runtime:2.10.3
co.touchlab:stately-concurrency-jvm:2.1.0
co.touchlab:stately-concurrency:2.1.0
co.touchlab:stately-concurrent-collections-jvm:2.1.0
co.touchlab:stately-concurrent-collections:2.1.0
co.touchlab:stately-strict-jvm:2.1.0
co.touchlab:stately-strict:2.1.0
com.android.billingclient:billing-ktx:7.1.1
com.android.billingclient:billing:7.1.1
com.beetstra.jutf7:jutf7:1.0.0
com.github.ByteHamster:SearchPreference:2.7.3
com.github.bumptech.glide:annotations:4.16.0
com.github.bumptech.glide:disklrucache:4.16.0
com.github.bumptech.glide:gifdecoder:4.16.0
com.github.bumptech.glide:glide:4.16.0
com.github.skydoves:landscapist-android:2.5.1
com.github.skydoves:landscapist-coil3-android:2.5.1
com.github.skydoves:landscapist-coil3:2.5.1
com.github.skydoves:landscapist:2.5.1
com.google.android.datatransport:transport-api:3.0.0
com.google.android.datatransport:transport-backend-cct:3.1.8
com.google.android.datatransport:transport-runtime:3.1.8
com.google.android.flexbox:flexbox:3.0.0
com.google.android.gms:play-services-base:18.5.0
com.google.android.gms:play-services-basement:18.4.0
com.google.android.gms:play-services-location:19.0.0
com.google.android.gms:play-services-places-placereport:17.0.0
com.google.android.gms:play-services-tasks:18.2.0
com.google.android.material:material:1.12.0
com.google.errorprone:error_prone_annotations:2.15.0
com.google.firebase:firebase-encoders-json:18.0.0
com.google.firebase:firebase-encoders-proto:16.0.0
com.google.firebase:firebase-encoders:17.0.0
com.google.guava:listenablefuture:1.0
com.jakewharton.timber:timber:5.0.1
com.jcraft:jzlib:1.0.7
com.mikepenz:fastadapter-extensions-drag:5.7.0
com.mikepenz:fastadapter-extensions-expandable:5.7.0
com.mikepenz:fastadapter-extensions-swipe:5.7.0
com.mikepenz:fastadapter-extensions-utils:5.7.0
com.mikepenz:fastadapter:5.7.0
com.squareup.moshi:moshi:1.15.2
com.squareup.okhttp3:okhttp:4.12.0
com.squareup.okio:okio-jvm:3.16.0
com.squareup.okio:okio:3.16.0
com.takisoft.colorpicker:colorpicker:1.0.0
com.takisoft.datetimepicker:datetimepicker:1.0.2
com.takisoft.preferencex:preferencex-colorpicker:1.1.0
com.takisoft.preferencex:preferencex-datetimepicker:1.1.0
com.takisoft.preferencex:preferencex:1.1.0
commons-io:commons-io:2.20.0
de.cketti.library.changelog:ckchangelog-core:2.0.0-beta02
de.cketti.safecontentresolver:safe-content-resolver-v21:1.0.0
de.hdodenhof:circleimageview:3.1.0
io.coil-kt.coil3:coil-android:3.2.0
io.coil-kt.coil3:coil-core-android:3.2.0
io.coil-kt.coil3:coil-core:3.2.0
io.coil-kt.coil3:coil-gif:3.2.0
io.coil-kt.coil3:coil-network-core-android:3.2.0
io.coil-kt.coil3:coil-network-core:3.2.0
io.coil-kt.coil3:coil-network-okhttp-jvm:3.2.0
io.coil-kt.coil3:coil-network-okhttp:3.2.0
io.coil-kt.coil3:coil-video:3.2.0
io.coil-kt.coil3:coil:3.2.0
io.insert-koin:koin-android:4.1.0
io.insert-koin:koin-androidx-compose:4.1.0
io.insert-koin:koin-bom:4.1.0
io.insert-koin:koin-compose-android:4.1.0
io.insert-koin:koin-compose-viewmodel-android:4.1.0
io.insert-koin:koin-compose-viewmodel:4.1.0
io.insert-koin:koin-compose:4.1.0
io.insert-koin:koin-core-jvm:4.1.0
io.insert-koin:koin-core-viewmodel-android:4.1.0
io.insert-koin:koin-core-viewmodel:4.1.0
io.insert-koin:koin-core:4.1.0
javax.inject:javax.inject:1
net.jcip:jcip-annotations:1.0
net.openid:appauth:0.11.1
org.apache.commons:commons-lang3:3.7
org.apache.commons:commons-text:1.3
org.apache.httpcomponents.client5:httpclient5:5.5
org.apache.httpcomponents.core5:httpcore5-h2:5.3.4
org.apache.httpcomponents.core5:httpcore5:5.3.4
org.apache.james:apache-mime4j-core:0.8.13
org.apache.james:apache-mime4j-dom:0.8.13
org.jetbrains.androidx.lifecycle:lifecycle-common:2.9.1
org.jetbrains.androidx.lifecycle:lifecycle-runtime-compose:2.9.1
org.jetbrains.androidx.lifecycle:lifecycle-runtime:2.9.1
org.jetbrains.androidx.lifecycle:lifecycle-viewmodel-compose:2.9.1
org.jetbrains.androidx.lifecycle:lifecycle-viewmodel-savedstate:2.9.1
org.jetbrains.androidx.lifecycle:lifecycle-viewmodel:2.9.1
org.jetbrains.androidx.savedstate:savedstate:1.3.1
org.jetbrains.compose.animation:animation-core:1.8.2
org.jetbrains.compose.animation:animation:1.8.2
org.jetbrains.compose.annotation-internal:annotation:1.8.2
org.jetbrains.compose.collection-internal:collection:1.8.2
org.jetbrains.compose.components:components-resources-android:1.8.2
org.jetbrains.compose.components:components-resources:1.8.2
org.jetbrains.compose.components:components-ui-tooling-preview-android:1.8.2
org.jetbrains.compose.components:components-ui-tooling-preview:1.8.2
org.jetbrains.compose.foundation:foundation-layout:1.8.2
org.jetbrains.compose.foundation:foundation:1.8.2
org.jetbrains.compose.runtime:runtime-saveable:1.8.2
org.jetbrains.compose.runtime:runtime:1.8.2
org.jetbrains.compose.ui:ui-geometry:1.8.2
org.jetbrains.compose.ui:ui-graphics:1.8.2
org.jetbrains.compose.ui:ui-text:1.8.2
org.jetbrains.compose.ui:ui-tooling-preview:1.8.2
org.jetbrains.compose.ui:ui-unit:1.8.2
org.jetbrains.compose.ui:ui-util:1.8.2
org.jetbrains.compose.ui:ui:1.8.2
org.jetbrains.kotlin:kotlin-android-extensions-runtime:2.2.0
org.jetbrains.kotlin:kotlin-bom:2.2.0
org.jetbrains.kotlin:kotlin-parcelize-runtime:2.2.0
org.jetbrains.kotlin:kotlin-stdlib-common:2.2.0
org.jetbrains.kotlin:kotlin-stdlib-jdk7:2.2.0
org.jetbrains.kotlin:kotlin-stdlib-jdk8:2.2.0
org.jetbrains.kotlin:kotlin-stdlib:2.2.0
org.jetbrains.kotlinx:kotlinx-collections-immutable-jvm:0.4.0
org.jetbrains.kotlinx:kotlinx-collections-immutable:0.4.0
org.jetbrains.kotlinx:kotlinx-coroutines-android:1.10.2
org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.10.2
org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.10.2
org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.2
org.jetbrains.kotlinx:kotlinx-datetime-jvm:0.7.1
org.jetbrains.kotlinx:kotlinx-datetime:0.7.1
org.jetbrains.kotlinx:kotlinx-io-bytestring-jvm:0.8.0
org.jetbrains.kotlinx:kotlinx-io-bytestring:0.8.0
org.jetbrains.kotlinx:kotlinx-io-core-jvm:0.8.0
org.jetbrains.kotlinx:kotlinx-io-core:0.8.0
org.jetbrains.kotlinx:kotlinx-serialization-bom:1.9.0
org.jetbrains.kotlinx:kotlinx-serialization-core-jvm:1.9.0
org.jetbrains.kotlinx:kotlinx-serialization-core:1.9.0
org.jetbrains.kotlinx:kotlinx-serialization-json-jvm:1.9.0
org.jetbrains.kotlinx:kotlinx-serialization-json:1.9.0
org.jetbrains:annotations:26.0.2
org.jsoup:jsoup:1.19.1
org.jspecify:jspecify:1.0.0
org.minidns:minidns-client:1.1.1
org.minidns:minidns-core:1.1.1
org.minidns:minidns-dnssec:1.1.1
org.minidns:minidns-hla:1.1.1
org.minidns:minidns-iterative-resolver:1.1.1
org.slf4j:slf4j-api:1.7.36

64
app-k9mail/proguard-rules.pro vendored Normal file
View file

@ -0,0 +1,64 @@
# Add project specific ProGuard rules here.
-dontobfuscate
# Preserve the line number information for debugging stack traces.
-keepattributes SourceFile,LineNumberTable
# Library specific rules
-dontnote android.net.http.*
-dontnote org.apache.commons.codec.**
-dontnote org.apache.http.**
-dontnote com.squareup.moshi.**
-dontnote com.github.amlcurran.showcaseview.**
-dontnote de.cketti.safecontentresolver.**
-dontnote com.tokenautocomplete.**
-dontwarn okio.**
-dontwarn com.squareup.moshi.**
# Glide
-keep public class * extends com.bumptech.glide.module.AppGlideModule
-keep public class * extends com.bumptech.glide.module.LibraryGlideModule
-keep public enum com.bumptech.glide.load.ImageHeaderParser$** {
**[] $VALUES;
public *;
}
# Project specific rules
-dontnote com.fsck.k9.ui.messageview.**
-dontnote com.fsck.k9.view.**
-assumevalues class * extends android.view.View {
boolean isInEditMode() return false;
}
-keep public class org.openintents.openpgp.**
-keepclassmembers class * extends androidx.appcompat.widget.SearchView {
public <init>(android.content.Context);
}
-keep class com.fsck.k9.mail.oauth.XOAuth2Response { *; }
# okhttp rules
# see: https://github.com/square/okhttp/blob/master/okhttp/src/main/resources/META-INF/proguard/okhttp3.pro
# JSR 305 annotations are for embedding nullability information.
-dontwarn javax.annotation.**
# A resource is loaded with a relative path so the package of this class must be preserved.
-keepnames class okhttp3.internal.publicsuffix.PublicSuffixDatabase
# Animal Sniffer compileOnly dependency to ensure APIs are compatible with older versions of Java.
-dontwarn org.codehaus.mojo.animal_sniffer.*
# OkHttp platform used only on JVM and when Conscrypt dependency is available.
-dontwarn okhttp3.internal.platform.ConscryptPlatform
-dontwarn kotlinx.serialization.KSerializer
-dontwarn kotlinx.serialization.Serializable
-dontwarn org.apache.http.client.methods.CloseableHttpResponse
-dontwarn org.slf4j.impl.StaticLoggerBinder
-keep,allowshrinking class com.tokenautocomplete.TokenCompleteTextView

View file

@ -0,0 +1,27 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
>
<application tools:ignore="MissingApplicationIcon">
<!-- This component is disabled by default (if possible). It will be enabled programmatically if necessary. -->
<!-- IMPORTANT: The component name must be -->
<!-- `net.thunderbird.feature.widget.message.list.MessageListWidgetReceiver` and can't be changed. -->
<receiver
android:name="net.thunderbird.feature.widget.message.list.MessageListWidgetReceiver"
android:exported="true"
android:label="@string/message_list_glance_widget_label"
>
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
</intent-filter>
<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/message_list_glance_widget_info"
/>
</receiver>
</application>
</manifest>

View file

@ -0,0 +1,92 @@
package app.k9mail.auth
import com.fsck.k9.BuildConfig
import net.thunderbird.core.common.oauth.OAuthConfiguration
import net.thunderbird.core.common.oauth.OAuthConfigurationFactory
@Suppress("ktlint:standard:max-line-length")
class K9OAuthConfigurationFactory : OAuthConfigurationFactory {
override fun createConfigurations(): Map<List<String>, OAuthConfiguration> {
return mapOf(
createAolConfiguration(),
createFastmailConfiguration(),
createGmailConfiguration(),
createMicrosoftConfiguration(),
createYahooConfiguration(),
)
}
private fun createAolConfiguration(): Pair<List<String>, OAuthConfiguration> {
return listOf(
"imap.aol.com",
"smtp.aol.com",
) to OAuthConfiguration(
clientId = "dj0yJmk9cHYydkJkTUxHcXlYJmQ9WVdrOWVHZHhVVXN4VVV3bWNHbzlNQT09JnM9Y29uc3VtZXJzZWNyZXQmc3Y9MCZ4PTdm",
scopes = listOf("mail-w"),
authorizationEndpoint = "https://api.login.aol.com/oauth2/request_auth",
tokenEndpoint = "https://api.login.aol.com/oauth2/get_token",
redirectUri = "${BuildConfig.APPLICATION_ID}://oauth2redirect",
)
}
private fun createFastmailConfiguration(): Pair<List<String>, OAuthConfiguration> {
return listOf(
"imap.fastmail.com",
"smtp.fastmail.com",
) to OAuthConfiguration(
clientId = "353641ae",
scopes = listOf("https://www.fastmail.com/dev/protocol-imap", "https://www.fastmail.com/dev/protocol-smtp"),
authorizationEndpoint = "https://api.fastmail.com/oauth/authorize",
tokenEndpoint = "https://api.fastmail.com/oauth/refresh",
redirectUri = "${BuildConfig.APPLICATION_ID}://oauth2redirect",
)
}
private fun createGmailConfiguration(): Pair<List<String>, OAuthConfiguration> {
return listOf(
"imap.gmail.com",
"imap.googlemail.com",
"smtp.gmail.com",
"smtp.googlemail.com",
) to OAuthConfiguration(
clientId = "262622259280-5qb3vtj68d5dtudmaif4g9vd3cpar8r3.apps.googleusercontent.com",
scopes = listOf("https://mail.google.com/"),
authorizationEndpoint = "https://accounts.google.com/o/oauth2/v2/auth",
tokenEndpoint = "https://oauth2.googleapis.com/token",
redirectUri = "${BuildConfig.APPLICATION_ID}:/oauth2redirect",
)
}
private fun createMicrosoftConfiguration(): Pair<List<String>, OAuthConfiguration> {
return listOf(
"outlook.office365.com",
"smtp.office365.com",
"smtp-mail.outlook.com",
) to OAuthConfiguration(
clientId = "e647013a-ada4-4114-b419-e43d250f99c5",
scopes = listOf(
"openid",
"email",
"https://outlook.office.com/IMAP.AccessAsUser.All",
"https://outlook.office.com/SMTP.Send",
"offline_access",
),
authorizationEndpoint = "https://login.microsoftonline.com/common/oauth2/v2.0/authorize",
tokenEndpoint = "https://login.microsoftonline.com/common/oauth2/v2.0/token",
redirectUri = "msauth://com.fsck.k9.debug/VZF2DYuLYAu4TurFd6usQB2JPts%3D",
)
}
private fun createYahooConfiguration(): Pair<List<String>, OAuthConfiguration> {
return listOf(
"imap.mail.yahoo.com",
"smtp.mail.yahoo.com",
) to OAuthConfiguration(
clientId = "dj0yJmk9ejRCRU1ybmZjQlVBJmQ9WVdrOVVrZEViak4xYmxZbWNHbzlNQT09JnM9Y29uc3VtZXJzZWNyZXQmc3Y9MCZ4PTZj",
scopes = listOf("mail-w"),
authorizationEndpoint = "https://api.login.yahoo.com/oauth2/request_auth",
tokenEndpoint = "https://api.login.yahoo.com/oauth2/get_token",
redirectUri = "${BuildConfig.APPLICATION_ID}://oauth2redirect",
)
}
}

View file

@ -0,0 +1,17 @@
package app.k9mail.dev
import app.k9mail.autodiscovery.api.AutoDiscovery
import app.k9mail.autodiscovery.demo.DemoAutoDiscovery
import com.fsck.k9.backend.BackendFactory
import org.koin.core.module.Module
import org.koin.core.qualifier.named
fun Module.developmentModuleAdditions() {
single { DemoBackendFactory(backendStorageFactory = get()) }
single<Map<String, BackendFactory>>(named("developmentBackends")) {
mapOf("demo" to get<DemoBackendFactory>())
}
single<List<AutoDiscovery>>(named("extraAutoDiscoveries")) {
listOf(DemoAutoDiscovery())
}
}

View file

@ -0,0 +1,14 @@
package app.k9mail.dev
import app.k9mail.backend.demo.DemoBackend
import com.fsck.k9.backend.BackendFactory
import com.fsck.k9.backend.api.Backend
import com.fsck.k9.mailstore.K9BackendStorageFactory
import net.thunderbird.core.android.account.LegacyAccount
class DemoBackendFactory(private val backendStorageFactory: K9BackendStorageFactory) : BackendFactory {
override fun createBackend(account: LegacyAccount): Backend {
val backendStorage = backendStorageFactory.createBackendStorage(account)
return DemoBackend(backendStorage)
}
}

View file

@ -0,0 +1,21 @@
package app.k9mail.featureflag
import net.thunderbird.core.featureflag.FeatureFlag
import net.thunderbird.core.featureflag.FeatureFlagFactory
import net.thunderbird.core.featureflag.FeatureFlagKey
import net.thunderbird.core.featureflag.toFeatureFlagKey
class K9FeatureFlagFactory : FeatureFlagFactory {
override fun createFeatureCatalog(): List<FeatureFlag> {
return listOf(
FeatureFlag("archive_marks_as_read".toFeatureFlagKey(), enabled = true),
FeatureFlag("new_account_settings".toFeatureFlagKey(), enabled = true),
FeatureFlag("disable_font_size_config".toFeatureFlagKey(), enabled = true),
FeatureFlag("email_notification_default".toFeatureFlagKey(), enabled = true),
FeatureFlag("enable_dropdown_drawer".toFeatureFlagKey(), enabled = true),
FeatureFlag("enable_dropdown_drawer_ui".toFeatureFlagKey(), enabled = true),
FeatureFlag(FeatureFlagKey.DisplayInAppNotifications, enabled = true),
FeatureFlag(FeatureFlagKey.UseNotificationSenderForSystemNotifications, enabled = true),
)
}
}

View file

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="app_logo_main">#5917ff</color>
<color name="app_logo_highlight_light">#7a45ff</color>
<color name="app_logo_highlight_dark">#531ad8</color>
<color name="launcher_icon_background">#e3d9ff</color>
</resources>

View file

@ -0,0 +1,82 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:installLocation="auto"
>
<application
android:name="app.k9mail.K9App"
android:icon="@drawable/ic_launcher"
android:label="@string/app_name"
android:theme="@style/Theme.K9.Startup"
tools:replace="android:theme"
>
<activity
android:name="com.fsck.k9.ui.settings.account.OpenPgpAppSelectDialog"
android:configChanges="locale"
android:theme="@style/Theme.K9.DayNight.Dialog.Translucent"
/>
<activity
android:name="com.fsck.k9.ui.notification.DeleteConfirmationActivity"
android:excludeFromRecents="true"
android:launchMode="singleTop"
android:taskAffinity=""
android:theme="@style/Theme.K9.DayNight.Dialog.Translucent"
/>
<provider
android:name="androidx.startup.InitializationProvider"
android:authorities="${applicationId}.androidx-startup"
android:exported="false"
tools:node="merge"
>
<!-- We're using on-demand initialization for WorkManager -->
<meta-data
android:name="androidx.work.WorkManagerInitializer"
android:value="androidx.startup"
tools:node="remove"
/>
</provider>
<!-- This component is disabled by default (if possible). It will be enabled programmatically if necessary. -->
<!-- IMPORTANT: The component name must be `com.fsck.k9.widget.list.MessageListWidgetProvider` and can't be changed. -->
<receiver
android:name="com.fsck.k9.widget.list.MessageListWidgetProvider"
android:icon="@drawable/message_list_widget_preview"
android:label="@string/message_list_widget_label"
android:enabled="@bool/home_screen_widgets_enabled"
android:exported="false"
>
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
</intent-filter>
<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/message_list_widget_info"
/>
</receiver>
<!-- This component is disabled by default (if possible). It will be enabled programmatically if necessary. -->
<!-- IMPORTANT: The component name must be `com.fsck.k9.provider.UnreadWidgetProvider` and can't be changed. -->
<receiver
android:name="com.fsck.k9.provider.UnreadWidgetProvider"
android:label="@string/unread_widget_label"
android:enabled="@bool/home_screen_widgets_enabled"
android:exported="false"
>
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
</intent-filter>
<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/unread_widget_info"
/>
</receiver>
</application>
</manifest>

View file

@ -0,0 +1,8 @@
package app.k9mail
import net.thunderbird.app.common.BaseApplication
import org.koin.core.module.Module
class K9App : BaseApplication() {
override fun provideAppModule(): Module = appModule
}

View file

@ -0,0 +1,45 @@
package app.k9mail
import app.k9mail.auth.K9OAuthConfigurationFactory
import app.k9mail.dev.developmentModuleAdditions
import app.k9mail.feature.featureModule
import app.k9mail.feature.widget.shortcut.LauncherShortcutActivity
import app.k9mail.featureflag.K9FeatureFlagFactory
import app.k9mail.provider.providerModule
import app.k9mail.widget.widgetModule
import com.fsck.k9.AppConfig
import com.fsck.k9.BuildConfig
import com.fsck.k9.DefaultAppConfig
import com.fsck.k9.activity.MessageCompose
import com.fsck.k9.provider.UnreadWidgetProvider
import com.fsck.k9.widget.list.MessageListWidgetProvider
import net.thunderbird.app.common.appCommonModule
import net.thunderbird.core.common.oauth.OAuthConfigurationFactory
import net.thunderbird.core.featureflag.FeatureFlagFactory
import org.koin.core.qualifier.named
import org.koin.dsl.module
val appModule = module {
includes(appCommonModule)
includes(widgetModule)
includes(featureModule)
includes(providerModule)
single(named("ClientInfoAppName")) { BuildConfig.CLIENT_INFO_APP_NAME }
single(named("ClientInfoAppVersion")) { BuildConfig.VERSION_NAME }
single<AppConfig> { appConfig }
single<OAuthConfigurationFactory> { K9OAuthConfigurationFactory() }
single<FeatureFlagFactory> { K9FeatureFlagFactory() }
developmentModuleAdditions()
}
val appConfig = DefaultAppConfig(
componentsToDisable = listOf(
MessageCompose::class.java,
LauncherShortcutActivity::class.java,
UnreadWidgetProvider::class.java,
MessageListWidgetProvider::class.java,
),
)

View file

@ -0,0 +1,21 @@
package app.k9mail.feature
import app.k9mail.feature.funding.api.FundingSettings
import app.k9mail.feature.funding.featureFundingModule
import app.k9mail.feature.migration.launcher.featureMigrationModule
import app.k9mail.feature.onboarding.migration.onboardingMigrationModule
import app.k9mail.feature.telemetry.telemetryModule
import net.thunderbird.feature.account.settings.featureAccountSettingsModule
import net.thunderbird.feature.mail.message.list.featureMessageListModule
import org.koin.dsl.module
val featureModule = module {
includes(featureAccountSettingsModule)
includes(telemetryModule)
includes(featureFundingModule)
includes(onboardingMigrationModule)
includes(featureMigrationModule)
includes(featureMessageListModule)
single<FundingSettings> { K9FundingSettings() }
}

View file

@ -0,0 +1,27 @@
package app.k9mail.feature
import app.k9mail.feature.funding.api.FundingSettings
import com.fsck.k9.K9
internal class K9FundingSettings : FundingSettings {
override fun getReminderReferenceTimestamp(): Long = K9.fundingReminderReferenceTimestamp
override fun setReminderReferenceTimestamp(timestamp: Long) {
K9.fundingReminderReferenceTimestamp = timestamp
K9.saveSettingsAsync()
}
override fun getReminderShownTimestamp() = K9.fundingReminderShownTimestamp
override fun setReminderShownTimestamp(timestamp: Long) {
K9.fundingReminderShownTimestamp = timestamp
K9.saveSettingsAsync()
}
override fun getActivityCounterInMillis(): Long = K9.fundingActivityCounterInMillis
override fun setActivityCounterInMillis(activeTime: Long) {
K9.fundingActivityCounterInMillis = activeTime
K9.saveSettingsAsync()
}
}

View file

@ -0,0 +1,21 @@
package app.k9mail.provider
import android.content.Context
import com.fsck.k9.R
import com.fsck.k9.preferences.FilePrefixProvider
import net.thunderbird.core.common.provider.AppNameProvider
import net.thunderbird.core.common.provider.BrandNameProvider
internal class K9AppNameProvider(
context: Context,
) : AppNameProvider, BrandNameProvider, FilePrefixProvider {
override val appName: String by lazy {
context.getString(R.string.app_name)
}
override val brandName: String by lazy {
context.getString(R.string.app_name)
}
override val filePrefix: String = "k9"
}

View file

@ -0,0 +1,21 @@
package app.k9mail.provider
import androidx.compose.runtime.Composable
import app.k9mail.core.ui.compose.theme2.k9mail.K9MailTheme2
import net.thunderbird.core.ui.theme.api.FeatureThemeProvider
internal class K9FeatureThemeProvider : FeatureThemeProvider {
@Composable
override fun WithTheme(content: @Composable () -> Unit) {
K9MailTheme2 {
content()
}
}
@Composable
override fun WithTheme(darkTheme: Boolean, content: @Composable () -> Unit) {
K9MailTheme2(darkTheme = darkTheme) {
content()
}
}
}

View file

@ -0,0 +1,12 @@
package app.k9mail.provider
import com.fsck.k9.R
import net.thunderbird.core.ui.theme.api.ThemeProvider
internal class K9ThemeProvider : ThemeProvider {
override val appThemeResourceId = R.style.Theme_K9_DayNight
override val appLightThemeResourceId = R.style.Theme_K9_Light
override val appDarkThemeResourceId = R.style.Theme_K9_Dark
override val dialogThemeResourceId = R.style.Theme_K9_DayNight_Dialog
override val translucentDialogThemeResourceId = R.style.Theme_K9_DayNight_Dialog_Translucent
}

View file

@ -0,0 +1,20 @@
package app.k9mail.provider
import com.fsck.k9.preferences.FilePrefixProvider
import net.thunderbird.core.common.provider.AppNameProvider
import net.thunderbird.core.common.provider.BrandNameProvider
import net.thunderbird.core.ui.theme.api.FeatureThemeProvider
import net.thunderbird.core.ui.theme.api.ThemeProvider
import org.koin.android.ext.koin.androidContext
import org.koin.dsl.binds
import org.koin.dsl.module
internal val providerModule = module {
single {
K9AppNameProvider(androidContext())
} binds arrayOf(AppNameProvider::class, BrandNameProvider::class, FilePrefixProvider::class)
single<ThemeProvider> { K9ThemeProvider() }
single<FeatureThemeProvider> { K9FeatureThemeProvider() }
}

View file

@ -0,0 +1,8 @@
package app.k9mail.widget
import app.k9mail.feature.widget.message.list.MessageListWidgetConfig
import com.fsck.k9.widget.list.MessageListWidgetProvider
class K9MessageListWidgetConfig : MessageListWidgetConfig {
override val providerClass = MessageListWidgetProvider::class.java
}

View file

@ -0,0 +1,8 @@
package app.k9mail.widget
import app.k9mail.feature.widget.unread.UnreadWidgetConfig
import com.fsck.k9.provider.UnreadWidgetProvider
class K9UnreadWidgetConfig : UnreadWidgetConfig {
override val providerClass = UnreadWidgetProvider::class.java
}

View file

@ -0,0 +1,13 @@
package app.k9mail.widget
import app.k9mail.feature.widget.message.list.MessageListWidgetConfig
import app.k9mail.feature.widget.unread.UnreadWidgetConfig
import net.thunderbird.feature.widget.message.list.featureWidgetMessageListModule
import org.koin.dsl.module
internal val widgetModule = module {
includes(featureWidgetMessageListModule)
single<MessageListWidgetConfig> { K9MessageListWidgetConfig() }
single<UnreadWidgetConfig> { K9UnreadWidgetConfig() }
}

View file

@ -0,0 +1,11 @@
package com.fsck.k9.provider
import app.k9mail.feature.widget.unread.BaseUnreadWidgetProvider
/**
* IMPORTANT: The fully qualified name for this class must be
* `com.fsck.k9.provider.UnreadWidgetProvider`.
* Otherwise widgets created with older versions of the app using a different name
* will stop working or disappear.
*/
class UnreadWidgetProvider : BaseUnreadWidgetProvider()

View file

@ -0,0 +1,11 @@
package com.fsck.k9.widget.list
import app.k9mail.feature.widget.message.list.BaseMessageListWidgetProvider
/**
* IMPORTANT: The fully qualified name for this class must be
* `com.fsck.k9.widget.list.MessageListWidgetProvider`.
* Otherwise widgets created with older versions of the app using a different name
* will stop working or disappear.
*/
class MessageListWidgetProvider : BaseMessageListWidgetProvider()

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>

View file

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="app_name">بريد K-9</string>
</resources>

View file

@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>

View file

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="app_name">K-9 Mail</string>
</resources>

View file

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="app_name">Пошта K-9</string>
</resources>

View file

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="app_name">K-9 Поща</string>
</resources>

View file

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">K-9 মেইল</string>
</resources>

View file

@ -0,0 +1,3 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
</resources>

View file

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="app_name">K-9 Mail</string>
</resources>

View file

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="app_name">K-9 Mail</string>
</resources>

View file

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="app_name">K-9 Mail</string>
</resources>

View file

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="app_name">K-9 Mail</string>
</resources>

View file

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="app_name">K-9 Mail</string>
</resources>

View file

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="app_name">K-9 Mail</string>
</resources>

View file

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="app_name">K-9 Mail</string>
</resources>

View file

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="app_name">K-9 Mail</string>
</resources>

View file

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="app_name">K-9 Mail</string>
</resources>

View file

@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>

View file

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="app_name">K-9 Retpoŝtilo</string>
</resources>

View file

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="app_name">K-9 Mail</string>
</resources>

View file

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="app_name">K-9 Mail</string>
</resources>

View file

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="app_name">K-9 Mail</string>
</resources>

View file

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="app_name">نامهٔ کی۹</string>
</resources>

View file

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="app_name">K-9 Mail</string>
</resources>

View file

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="app_name">Courriel K-9</string>
</resources>

View file

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="app_name">K-9 Mail</string>
</resources>

View file

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">K-9 Post</string>
</resources>

View file

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="app_name">Post K-9</string>
</resources>

View file

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="app_name">K-9 Mail</string>
</resources>

View file

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">કે-૯ મેલ</string>
</resources>

View file

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="app_name">के-9 मेल</string>
</resources>

View file

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="app_name">K-9 Mail</string>
</resources>

View file

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="app_name">K-9 Mail</string>
</resources>

View file

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="app_name">K-9 Նամակ</string>
</resources>

View file

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="app_name">Surel K-9</string>
</resources>

View file

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="app_name">K-9 - Póstur</string>
</resources>

View file

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="app_name">K-9 Mail</string>
</resources>

View file

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="app_name">K-9 דוא\"ל</string>
</resources>

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