Compare commits

...

137 Commits
0.32 ... 0.41

Author SHA1 Message Date
M66B
7ee9e0ec6c 0.41 release 2018-09-07 16:30:46 +00:00
M66B
886cad559c Added menu to mark read/unread, star/unstar 2018-09-07 16:28:15 +00:00
M66B
5fb89f3d3c 0.40 release 2018-09-07 15:18:38 +00:00
M66B
268f8c9bbe Crowdin sync 2018-09-07 15:17:40 +00:00
M66B
7dd50cebab Added star/unstar
Fixes #9
2018-09-07 15:12:43 +00:00
M66B
22fb136cc9 Updated FAQ 2018-09-07 14:02:12 +00:00
M66B
88b167c50d Added navigation menu separators
Fixes #95
2018-09-07 13:58:04 +00:00
M66B
07dd6f0df6 Decrease start backk-off time to 8 seconds 2018-09-07 13:54:32 +00:00
M66B
2377f536a3 Updated FAQ
Closes #53
Closes #94
2018-09-07 13:36:44 +00:00
M66B
7b353482f6 Name placeholder for standard replies 2018-09-07 13:04:54 +00:00
M66B
e8b8a8a71c Small improvement 2018-09-07 13:04:34 +00:00
M66B
6859816e25 0.39 release 2018-09-07 12:50:52 +00:00
M66B
6b9082e209 Crowin sync 2018-09-07 12:49:26 +00:00
M66B
8c2efa2486 Layout improvements 2018-09-07 12:34:54 +00:00
M66B
b948eb45a9 Added provider Telstra 2018-09-07 11:26:34 +00:00
M66B
b341f41c8f Drafts should not be marked read 2018-09-07 11:22:58 +00:00
M66B
77f16a0caf Fixed multi line signatures 2018-09-07 11:12:46 +00:00
M66B
1d9a89d0b3 Added rate menu 2018-09-07 11:08:20 +00:00
M66B
ef804f4931 Fixed editing of custom accounts 2018-09-07 10:45:43 +00:00
M66B
a4ca55d7c6 Updated text 2018-09-07 10:26:49 +00:00
M66B
6d41950910 Added issue template 2018-09-07 10:05:06 +00:00
M66B
87eb10e134 Added FAQ 2018-09-07 09:51:01 +00:00
M66B
0f6bfc6ecb 0.38 releae 2018-09-07 09:06:34 +00:00
M66B
164129f09d Delete drafts instead of trash
Reason: A51 NO [ALERT] Cannot MOVE messages out of the Drafts folder
2018-09-07 09:02:45 +00:00
M66B
f5e67369ee Added option for signatures
Fixes #58
2018-09-07 08:46:00 +00:00
M66B
5382b0c022 Fixed typo 2018-09-07 06:55:51 +00:00
M66B
ee08ec2597 Set first new account/identity primary only 2018-09-07 06:52:19 +00:00
M66B
e1447ad132 Handle message removed while getting attachments 2018-09-06 12:23:54 +00:00
M66B
03e5519715 Add remarks about POP and insecure connections 2018-09-06 12:02:20 +00:00
M66B
753f39ce93 Updated FAQ 2018-09-06 11:06:57 +00:00
M66B
6082d1789d 0.37 release 2018-09-06 10:27:57 +00:00
M66B
2b0dc59829 Handle parse exception 2018-09-06 10:24:30 +00:00
M66B
f8cc649da5 Fixed some warnings 2018-09-06 09:52:02 +00:00
M66B
ba96b02f84 Drop support for encryption
https://forum.xda-developers.com/showpost.php?p=77544698&postcount=789

Closes #76
Closes #93
2018-09-06 09:44:36 +00:00
M66B
6a026b772a Handle folder closed 2018-09-06 09:40:21 +00:00
M66B
6e66ddcd53 Setup improvements 2018-09-06 07:29:30 +00:00
M66B
96df682541 Silently ignore broken messages 2018-09-06 07:16:33 +00:00
M66B
17c37807bd Reply all 2018-09-06 05:49:17 +00:00
M66B
30be70fec6 Revert "Resume last sync"
This reverts commit 62ae0eeb17.
2018-09-06 05:33:04 +00:00
M66B
62ae0eeb17 Resume last sync 2018-09-05 21:05:03 +00:00
M66B
deaa30a5f3 Fixes 2018-09-05 20:39:47 +00:00
M66B
86c9bea815 Simplify identity setup
Refs #52
2018-09-05 20:04:23 +00:00
M66B
8ac94776ed Fixed stop with multiple accounts 2018-09-05 19:18:33 +00:00
M66B
403fce8b52 Log daily cleanup 2018-09-05 19:02:54 +00:00
M66B
c0d4b7001a Skip token refresh account/identity setup 2018-09-05 19:00:21 +00:00
M66B
a5d77946ce Simplified account setup
Refs #52
2018-09-05 18:37:20 +00:00
Marcel Bokhorst
0d992cc914 Merge pull request #91 from necioerrante/faq-missing-colon
Add missing colon to list in FAQ.
2018-09-05 19:15:02 +02:00
necioerrante
5a140fb749 Add missing colon to list in FAQ.
Add the missing colon in FAQ question 3 list to keep same format as the other items in list.
2018-09-05 08:27:42 -08:00
M66B
108bd1159a Fixed reply to self, layout improvements 2018-09-05 16:18:46 +00:00
M66B
cf0e6386ba Show account is authorizing 2018-09-05 12:37:27 +00:00
M66B
f0a976e25a 0.36 release 2018-09-05 10:23:44 +00:00
M66B
9fe9879c72 Crowdin sync 2018-09-05 10:21:16 +00:00
M66B
1b5049c3d7 Changed icon
Closes #25
2018-09-05 10:17:47 +00:00
M66B
9ee2a28e02 Suppress unknown host exception 2018-09-05 09:47:22 +00:00
M66B
1e7ff72e55 Select folders to show in unified inbox
Fixed #47
Fixes #87
2018-09-05 09:41:16 +00:00
M66B
82d2c7e03a Fixed race condition 2018-09-05 09:37:56 +00:00
M66B
51e256ab9c Added provider web.de 2018-09-05 08:22:05 +00:00
M66B
e50452a6e6 0.35 release 2018-09-05 08:08:30 +00:00
M66B
6b21124d6e Crowdin sync 2018-09-05 08:08:11 +00:00
M66B
f2dae6438a Guess better content type 2018-09-05 07:57:34 +00:00
M66B
ebf41cdc76 Sanitize email name 2018-09-05 07:44:50 +00:00
M66B
9983addfdd Fixed crash 2018-09-05 07:38:53 +00:00
M66B
49f7a61717 Added option to show headers 2018-09-05 07:24:05 +00:00
M66B
cfb68b904c Handle exceptions without message 2018-09-04 19:45:20 +00:00
M66B
3f440777e5 Updated description 2018-09-04 19:00:58 +00:00
M66B
01710cbcb7 Prevent crash 2018-09-04 18:56:56 +00:00
M66B
d8c2c41a67 Refactoring 2018-09-04 18:51:09 +00:00
M66B
7d86e94613 Encrypt multiple recipients 2018-09-04 18:19:07 +00:00
M66B
ff6ea4af09 Setup doze / data saver 2018-09-04 18:06:22 +00:00
M66B
5eab316b47 Support viewport meta tag 2018-09-04 15:11:00 +00:00
M66B
f6291f8d7a Use custom tabs to open links in original message 2018-09-04 15:06:09 +00:00
M66B
28f7400d8a 0.34 release 2018-09-04 14:30:44 +00:00
M66B
1cc63476b6 Added conversation to legend 2018-09-04 14:18:55 +00:00
M66B
75697fe57d Cleanup found messages 2018-09-04 14:07:50 +00:00
M66B
13de85a3d3 Show swipe action icons
Fixes #59
2018-09-04 13:38:47 +00:00
M66B
52158a8672 Suppress connection failure messages 2018-09-04 12:40:59 +00:00
M66B
9ddecc91f7 Log full stack trace 2018-09-04 12:33:59 +00:00
M66B
c692777d66 Setup accounts before setting up identities
Fixes #73
2018-09-04 12:32:23 +00:00
M66B
390074a2c3 Offload logging 2018-09-04 12:31:50 +00:00
M66B
fb40704cb8 Updated FAQ 2018-09-04 11:45:11 +00:00
M66B
4bd931c29f Added option to show orginal message
Fixes #64
2018-09-04 11:26:18 +00:00
M66B
fe86f757f0 Fixed send-to
Fixes #63
2018-09-04 10:33:27 +00:00
M66B
694db33bb0 0.33 release 2018-09-04 09:03:23 +00:00
M66B
233e00b750 Style buttons/checkboxes 2018-09-04 08:50:08 +00:00
M66B
78ad5f14b0 Auto download attachments < 32 KB
Fixes #81
2018-09-04 08:28:29 +00:00
M66B
27be14082d Updated description 2018-09-04 08:14:26 +00:00
M66B
e3c9bcc6a3 Refactoring 2018-09-04 08:08:57 +00:00
M66B
8936223b84 Small improvements 2018-09-04 07:56:30 +00:00
M66B
eace1ed600 Suppress disconnected state for non syncing folders only 2018-09-04 07:06:23 +00:00
M66B
e8da25a674 Added daily cleanup job 2018-09-04 07:02:54 +00:00
M66B
4802a8b2c8 Improved logging 2018-09-04 05:49:58 +00:00
M66B
de266b1cde Check for single recipient when encrypting 2018-09-04 05:10:46 +00:00
M66B
2403be2231 Added basic log 2018-09-03 19:26:32 +00:00
M66B
e7d7d88bef Small improvements 2018-09-03 19:08:50 +00:00
M66B
f50729240d Increase logcat size 2018-09-03 18:24:26 +00:00
M66B
0a4c69944a Disabled attachment item animations 2018-09-03 18:22:10 +00:00
M66B
4dc86f78b2 Free message memory 2018-09-03 18:16:08 +00:00
M66B
ae3e0c64ce Cleanup, small improvements 2018-09-03 18:08:50 +00:00
M66B
fee57e5426 Small improvements 2018-09-03 17:58:04 +00:00
M66B
c49e24c8b4 Fixed composing message disappearing 2018-09-03 17:57:06 +00:00
M66B
2667816ecb Added provider Yandex 2018-09-03 17:10:46 +00:00
M66B
a0880c4c06 Message can change folder 2018-09-03 17:07:03 +00:00
M66B
87e8e713db Small improvements 2018-09-03 16:34:38 +00:00
M66B
7d8202f68e Updated description 2018-09-03 09:52:13 +00:00
M66B
ce29fa7c6b Folder state fixes 2018-09-03 09:34:13 +00:00
M66B
28b01666c6 Fixed crash 2018-09-03 09:13:12 +00:00
M66B
a64b7999f2 View encrypted message improvements 2018-09-03 08:43:44 +00:00
M66B
c75079a5ce Use cached thread pool for simple task 2018-09-03 08:43:31 +00:00
M66B
0c4566ddcf Send encrypted messages with attachments 2018-09-03 08:09:34 +00:00
M66B
cbc2d98d52 Workaround profiler bug 2018-09-03 08:04:51 +00:00
M66B
f06b0c10e6 Notify complete exception 2018-09-03 04:46:44 +00:00
M66B
9490c22914 Linkify html 2018-09-02 11:18:32 +00:00
M66B
3488f2ee8e Fixed crash / select from start 2018-09-02 10:30:45 +00:00
M66B
c274502392 Revert "Simplification"
This reverts commit 15f4af4708.
2018-09-02 10:28:32 +00:00
M66B
c2e5378a74 Fixed outbox state 2018-09-02 09:59:54 +00:00
M66B
f9fe3621a6 Use executor instead of thread 2018-09-02 08:55:06 +00:00
M66B
0dc45a8d1b Fixed notification newlines 2018-09-02 08:36:02 +00:00
M66B
f634d57404 Suppress socket exception 2018-09-02 08:34:06 +00:00
M66B
c588640c7b Prevent get/set seen race condition 2018-09-02 08:31:45 +00:00
M66B
bd586f7370 Refresh token on search 2018-09-02 08:24:09 +00:00
M66B
b5d71b783c Made simple task multi threading 2018-09-02 08:22:04 +00:00
M66B
e64a43ff64 Progressive search improvements 2018-09-02 08:21:43 +00:00
M66B
f207a7deb9 Handle HTML images without source 2018-09-02 05:45:37 +00:00
M66B
12b801711f Progressive search improvements, crowdin sync 2018-09-01 20:01:40 +00:00
M66B
9409db25d6 Reply to recipient, not to known self 2018-09-01 19:44:01 +00:00
M66B
131beadea9 Progressive search 2018-09-01 19:15:25 +00:00
M66B
6d3c4a96fa Support for encrypted attachments
Refs #66
2018-09-01 15:00:28 +00:00
M66B
7e3f4563d1 Better decryption handling
Refs #66
2018-09-01 13:51:04 +00:00
M66B
b68aba25b7 Use recipient address for encryption
Fixes #43
2018-09-01 11:07:14 +00:00
M66B
0698e9dd6b Added provider 2018-09-01 07:50:35 +00:00
M66B
15f4af4708 Simplification 2018-09-01 07:20:27 +00:00
M66B
2d7566ffc1 Prevent crash 2018-09-01 07:20:09 +00:00
M66B
cfeccc6f62 No decrypt in outbox 2018-09-01 06:35:00 +00:00
M66B
e6e8916e3d Show thread menu always iconified 2018-09-01 06:29:35 +00:00
M66B
80755d85d6 Long press to mark read/unread 2018-09-01 06:28:55 +00:00
M66B
4e222ec6d1 Fixed replying to self 2018-09-01 06:11:36 +00:00
M66B
8cbcc864e3 Removed poll support 2018-09-01 06:01:02 +00:00
132 changed files with 11106 additions and 2647 deletions

View File

@@ -34,7 +34,7 @@
<PersistentState>
<option name="values">
<map>
<entry key="scalingPercent" value="78" />
<entry key="scalingPercent" value="75" />
<entry key="trimmed" value="true" />
</map>
</option>
@@ -47,7 +47,7 @@
<map>
<entry key="backgroundAssetType" value="COLOR" />
<entry key="backgroundColor" value="cccccc" />
<entry key="foregroundImage" value="$PROJECT_DIR$/images/6_foreground_3_bis.png" />
<entry key="foregroundImage" value="$PROJECT_DIR$/images/4.png" />
<entry key="webIconShape" value="NONE" />
</map>
</option>

Binary file not shown.

60
FAQ.md
View File

@@ -1,8 +1,10 @@
FairEmail
=========
# FairEmail
Frequently Asked Questions
--------------------------
If you have a feature request or found a bug, you can report it [as an issue](https://github.com/M66B/open-source-email/issues).
If you have a question, please check the frequently asked questions below first. At the bottom you can find how to ask other questions.
## Frequently Asked Questions
<a name="FAQ1"></a>
**(1) Which permissions are needed and why?**
@@ -28,12 +30,14 @@ Most, if not all, other email apps don't show a notification with the "side effe
The low priority status bar notification shows the number of pending operations, which can be:
* SEEN: mark message as seen/unseen in remote folder
* ADD: add message to remote folder
* MOVE: move message to another remote folder
* DELETE: delete message from remote folder
* SEND: send message
* ATTACHMENT download attachment
* seen: mark message as seen/unseen in remote folder
* add: add message to remote folder
* move: move message to another remote folder
* delete: delete message from remote folder
* send: send message
* attachment: download attachment
* headers: download message headers
* flag: star/unstar remote message
<a name="FAQ4"></a>
**(4) What is a valid security certificate?**
@@ -45,6 +49,9 @@ Valid security certificates are officially signed (not self signed) and have mat
Without [IMAP IDLE](https://en.wikipedia.org/wiki/IMAP_IDLE) emails need to be periodically fetched,
which is a waste of battery power and internet bandwidth and will delay notification of new emails.
Since the goal of FairEmail is to offer safe and fast email, providers without IMAP IDLE are not supported.
You should consider this a problem of the provider, not of the app.
Almost all email providers offer IMAP IDLE, with as notable exception Yahoo!
<a name="FAQ6"></a>
**(6) How can I login to Gmail / G suite?**
@@ -100,29 +107,30 @@ The latter is both safer and more inconvenient because you'll need to login to w
Chrome Custom Tabs are used by default, which can be changed in the advanced options in the setup screen.
<a name="FAQ13"></a>
**(13) How does search on server work?**
**(13) How does progressive search work?**
You can start searching for messages in a folder on the server by using the magnify glass in the action bar of a folder.
The server is requested to search on sender, subject and message text.
The server executes the search request and determines if the search is case sensitive,
if searching will be done on whole words and which messages will be search through.
Results will be shown in real time as they become available from the server.
For performance reasons attachments are not downloaded and shown.
Search on server is a pro feature.
You can start searching for messages on sender, subject or text by using the magnify glass in the action bar of a folder.
First messages are searched on device, then the server is requested to search.
Scrolling down will download more messages from the server.
Searching on device is case insensitive and on partial text.
Searching on the server might be case sensitive or case insensitive and might be on partial text or whole words, depending on the provider.
Progressive search is a pro feature.
<a name="FAQ14"></a>
**(14) How does openPGP integration work?**
**(14) How can I setup Outlook with 2FA?**
You need to install and setup the [OpenKeychain](https://play.google.com/store/apps/details?id=org.sufficientlysecure.keychain).
To use Outlook with two factor authentication enabled, you need to create an app password.
See [here](https://support.microsoft.com/en-us/help/12409/microsoft-account-app-passwords-two-step-verification) for the details.
You can send an encrypted message by composing a message as usual and encrypt it by using the *Encrypt* overflow menu just before sending.
<a name="FAQ15"></a>
**(15) Can you add ... ?**
If you received an encrypted message as attachment, you can decrypt it by downloading and viewing the attachment with the encrypted text.
If you received an encrypted message with inline encryption, you can decrypt it by using the *Decrypt* overflow menu.
* More themes / account colors: the goal is to keep the app as simple as possible, so this will not be added.
* LED notifications: there are less and less devices with a notification light and if there is one, it can mostly be managed by Android, so there is little point in adding support for this.
* Encryption: there is too little interest in sending/receiving encrypted messages to justifiy putting effort into this.
* POP/poll support: besides that any decent provider is supporting / should support IMAP, polling does consume extra battery power and will delay notification of new messages, so this will not be added.
<br>
If you have another question, you can use [this forum](https://forum.xda-developers.com/android/apps-games/source-email-t3824168).
If you have a feature request or found a bug, you can report it [as an issue](https://github.com/M66B/open-source-email/issues).
Registration is free.

30
ISSUE_TEMPLATE.md Normal file
View File

@@ -0,0 +1,30 @@
# Question
* For questions, please use [this forum](https://forum.xda-developers.com/android/apps-games/source-email-t3824168).
# Feature request
* Did you check if there wasn't a similar feature request?
* Did you read [this FAQ](https://github.com/M66B/open-source-email/blob/master/FAQ.md#FAQ15)?
# Bug report
* Did you check if there wasn't a similar bug report?
* Are you using the [latest version](https://github.com/M66B/open-source-email/releases) of the app?
## Expected behavior
## Actual behavior
## Steps to reproduce the problem
1.
1.
1.
## Version
* App version:
* Android version:

View File

@@ -7,7 +7,7 @@ This email app might be for you if your current email app:
* takes long to receive or show messages
* can manage only one mailbox
* cannot show related messages
* cannot show conversations
* cannot work offline
* looks outdated
* is not maintained
@@ -31,10 +31,10 @@ Features
Pro features
------------
* Signatures
* Standard replies
* Progressive search (first local, then server)
* Preview sender/subject in new messages status bar notification
* Encrypt/decrypt messages using [OpenPGP](https://www.openpgp.org/)
* Search on server
* Standard answers
Simple
------
@@ -48,8 +48,8 @@ Secure
* Allow encrypted connections only
* Accept valid security certificates only
* [SMTP](https://en.wikipedia.org/wiki/Simple_Mail_Transfer_Protocol) authentication required
* Text view only (converted [HTML](https://en.wikipedia.org/wiki/HTML))
* Authentication required
* Optional text view only (converted [HTML](https://en.wikipedia.org/wiki/HTML))
* No special permissions required
* No advertisements
* No analytics and no tracking
@@ -57,7 +57,7 @@ Secure
Efficient
---------
* [IMAP IDLE](https://en.wikipedia.org/wiki/IMAP_IDLE) supported
* [IMAP IDLE](https://en.wikipedia.org/wiki/IMAP_IDLE) (push messages) supported
* Built with latest development tools and libraries
* Android 6 Marshmallow or later required

View File

@@ -6,8 +6,8 @@ android {
applicationId "eu.faircode.email"
minSdkVersion 23
targetSdkVersion 28
versionCode 32
versionName "0.32"
versionCode 41
versionName "0.41"
archivesBaseName = "FairEmail-v$versionName"
javaCompileOptions {
@@ -19,11 +19,13 @@ android {
buildTypes {
release {
debuggable = false
minifyEnabled = true
useProguard = true
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
debug {
debuggable = true
minifyEnabled = true
useProguard = true
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
@@ -78,7 +80,6 @@ dependencies {
def javamail_version = "1.6.0"
def jsoup_version = "1.11.3"
def jcharset_version = "2.0"
def openpgp_version = "12.0"
implementation "androidx.appcompat:appcompat:$androidx_version"
implementation "androidx.recyclerview:recyclerview:$androidx_version"
@@ -102,6 +103,4 @@ dependencies {
implementation "org.jsoup:jsoup:$jsoup_version"
implementation "net.freeutils:jcharset:$jcharset_version"
implementation "org.sufficientlysecure:openpgp-api:$openpgp_version"
}

View File

@@ -0,0 +1,919 @@
{
"formatVersion": 1,
"database": {
"version": 10,
"identityHash": "8a267f8f3cb9ef409377dbdb7cb706d4",
"entities": [
{
"tableName": "identity",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `name` TEXT NOT NULL, `email` TEXT NOT NULL, `replyto` TEXT, `account` INTEGER NOT NULL, `host` TEXT NOT NULL, `port` INTEGER NOT NULL, `starttls` INTEGER NOT NULL, `user` TEXT NOT NULL, `password` TEXT NOT NULL, `auth_type` INTEGER NOT NULL, `primary` INTEGER NOT NULL, `synchronize` INTEGER NOT NULL, `store_sent` INTEGER NOT NULL, `state` TEXT, `error` TEXT, FOREIGN KEY(`account`) REFERENCES `account`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "email",
"columnName": "email",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "replyto",
"columnName": "replyto",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "account",
"columnName": "account",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "host",
"columnName": "host",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "port",
"columnName": "port",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "starttls",
"columnName": "starttls",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "user",
"columnName": "user",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "password",
"columnName": "password",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "auth_type",
"columnName": "auth_type",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "primary",
"columnName": "primary",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "synchronize",
"columnName": "synchronize",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "store_sent",
"columnName": "store_sent",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "state",
"columnName": "state",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "error",
"columnName": "error",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": true
},
"indices": [
{
"name": "index_identity_account",
"unique": false,
"columnNames": [
"account"
],
"createSql": "CREATE INDEX `index_identity_account` ON `${TABLE_NAME}` (`account`)"
}
],
"foreignKeys": [
{
"table": "account",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"account"
],
"referencedColumns": [
"id"
]
}
]
},
{
"tableName": "account",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `name` TEXT, `host` TEXT NOT NULL, `port` INTEGER NOT NULL, `user` TEXT NOT NULL, `password` TEXT NOT NULL, `auth_type` INTEGER NOT NULL, `primary` INTEGER NOT NULL, `synchronize` INTEGER NOT NULL, `store_sent` INTEGER NOT NULL, `poll_interval` INTEGER NOT NULL, `seen_until` INTEGER, `state` TEXT, `error` TEXT)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "host",
"columnName": "host",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "port",
"columnName": "port",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "user",
"columnName": "user",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "password",
"columnName": "password",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "auth_type",
"columnName": "auth_type",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "primary",
"columnName": "primary",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "synchronize",
"columnName": "synchronize",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "store_sent",
"columnName": "store_sent",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "poll_interval",
"columnName": "poll_interval",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "seen_until",
"columnName": "seen_until",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "state",
"columnName": "state",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "error",
"columnName": "error",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": true
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "folder",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `account` INTEGER, `name` TEXT NOT NULL, `type` TEXT NOT NULL, `unified` INTEGER NOT NULL, `synchronize` INTEGER NOT NULL, `after` INTEGER NOT NULL, `state` TEXT, `error` TEXT, FOREIGN KEY(`account`) REFERENCES `account`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "account",
"columnName": "account",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "type",
"columnName": "type",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "unified",
"columnName": "unified",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "synchronize",
"columnName": "synchronize",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "after",
"columnName": "after",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "state",
"columnName": "state",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "error",
"columnName": "error",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": true
},
"indices": [
{
"name": "index_folder_account_name",
"unique": true,
"columnNames": [
"account",
"name"
],
"createSql": "CREATE UNIQUE INDEX `index_folder_account_name` ON `${TABLE_NAME}` (`account`, `name`)"
},
{
"name": "index_folder_account",
"unique": false,
"columnNames": [
"account"
],
"createSql": "CREATE INDEX `index_folder_account` ON `${TABLE_NAME}` (`account`)"
},
{
"name": "index_folder_name",
"unique": false,
"columnNames": [
"name"
],
"createSql": "CREATE INDEX `index_folder_name` ON `${TABLE_NAME}` (`name`)"
},
{
"name": "index_folder_type",
"unique": false,
"columnNames": [
"type"
],
"createSql": "CREATE INDEX `index_folder_type` ON `${TABLE_NAME}` (`type`)"
},
{
"name": "index_folder_unified",
"unique": false,
"columnNames": [
"unified"
],
"createSql": "CREATE INDEX `index_folder_unified` ON `${TABLE_NAME}` (`unified`)"
}
],
"foreignKeys": [
{
"table": "account",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"account"
],
"referencedColumns": [
"id"
]
}
]
},
{
"tableName": "message",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `account` INTEGER, `folder` INTEGER NOT NULL, `identity` INTEGER, `replying` INTEGER, `uid` INTEGER, `msgid` TEXT, `references` TEXT, `inreplyto` TEXT, `thread` TEXT, `from` TEXT, `to` TEXT, `cc` TEXT, `bcc` TEXT, `reply` TEXT, `headers` TEXT, `subject` TEXT, `sent` INTEGER, `received` INTEGER NOT NULL, `stored` INTEGER NOT NULL, `seen` INTEGER NOT NULL, `ui_seen` INTEGER NOT NULL, `ui_hide` INTEGER NOT NULL, `ui_found` INTEGER NOT NULL, `error` TEXT, FOREIGN KEY(`account`) REFERENCES `account`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`folder`) REFERENCES `folder`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`identity`) REFERENCES `identity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`replying`) REFERENCES `message`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "account",
"columnName": "account",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "folder",
"columnName": "folder",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "identity",
"columnName": "identity",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "replying",
"columnName": "replying",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "uid",
"columnName": "uid",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "msgid",
"columnName": "msgid",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "references",
"columnName": "references",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "inreplyto",
"columnName": "inreplyto",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "thread",
"columnName": "thread",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "from",
"columnName": "from",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "to",
"columnName": "to",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "cc",
"columnName": "cc",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "bcc",
"columnName": "bcc",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "reply",
"columnName": "reply",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "headers",
"columnName": "headers",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "subject",
"columnName": "subject",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "sent",
"columnName": "sent",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "received",
"columnName": "received",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "stored",
"columnName": "stored",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "seen",
"columnName": "seen",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "ui_seen",
"columnName": "ui_seen",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "ui_hide",
"columnName": "ui_hide",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "ui_found",
"columnName": "ui_found",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "error",
"columnName": "error",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": true
},
"indices": [
{
"name": "index_message_account",
"unique": false,
"columnNames": [
"account"
],
"createSql": "CREATE INDEX `index_message_account` ON `${TABLE_NAME}` (`account`)"
},
{
"name": "index_message_folder",
"unique": false,
"columnNames": [
"folder"
],
"createSql": "CREATE INDEX `index_message_folder` ON `${TABLE_NAME}` (`folder`)"
},
{
"name": "index_message_identity",
"unique": false,
"columnNames": [
"identity"
],
"createSql": "CREATE INDEX `index_message_identity` ON `${TABLE_NAME}` (`identity`)"
},
{
"name": "index_message_replying",
"unique": false,
"columnNames": [
"replying"
],
"createSql": "CREATE INDEX `index_message_replying` ON `${TABLE_NAME}` (`replying`)"
},
{
"name": "index_message_folder_uid",
"unique": true,
"columnNames": [
"folder",
"uid"
],
"createSql": "CREATE UNIQUE INDEX `index_message_folder_uid` ON `${TABLE_NAME}` (`folder`, `uid`)"
},
{
"name": "index_message_msgid_folder",
"unique": true,
"columnNames": [
"msgid",
"folder"
],
"createSql": "CREATE UNIQUE INDEX `index_message_msgid_folder` ON `${TABLE_NAME}` (`msgid`, `folder`)"
},
{
"name": "index_message_thread",
"unique": false,
"columnNames": [
"thread"
],
"createSql": "CREATE INDEX `index_message_thread` ON `${TABLE_NAME}` (`thread`)"
},
{
"name": "index_message_received",
"unique": false,
"columnNames": [
"received"
],
"createSql": "CREATE INDEX `index_message_received` ON `${TABLE_NAME}` (`received`)"
},
{
"name": "index_message_ui_seen",
"unique": false,
"columnNames": [
"ui_seen"
],
"createSql": "CREATE INDEX `index_message_ui_seen` ON `${TABLE_NAME}` (`ui_seen`)"
},
{
"name": "index_message_ui_hide",
"unique": false,
"columnNames": [
"ui_hide"
],
"createSql": "CREATE INDEX `index_message_ui_hide` ON `${TABLE_NAME}` (`ui_hide`)"
},
{
"name": "index_message_ui_found",
"unique": false,
"columnNames": [
"ui_found"
],
"createSql": "CREATE INDEX `index_message_ui_found` ON `${TABLE_NAME}` (`ui_found`)"
}
],
"foreignKeys": [
{
"table": "account",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"account"
],
"referencedColumns": [
"id"
]
},
{
"table": "folder",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"folder"
],
"referencedColumns": [
"id"
]
},
{
"table": "identity",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"identity"
],
"referencedColumns": [
"id"
]
},
{
"table": "message",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"replying"
],
"referencedColumns": [
"id"
]
}
]
},
{
"tableName": "attachment",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `message` INTEGER NOT NULL, `sequence` INTEGER NOT NULL, `name` TEXT, `type` TEXT NOT NULL, `size` INTEGER, `progress` INTEGER, `available` INTEGER NOT NULL, FOREIGN KEY(`message`) REFERENCES `message`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "message",
"columnName": "message",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "sequence",
"columnName": "sequence",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "type",
"columnName": "type",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "size",
"columnName": "size",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "progress",
"columnName": "progress",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "available",
"columnName": "available",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": true
},
"indices": [
{
"name": "index_attachment_message",
"unique": false,
"columnNames": [
"message"
],
"createSql": "CREATE INDEX `index_attachment_message` ON `${TABLE_NAME}` (`message`)"
},
{
"name": "index_attachment_message_sequence",
"unique": true,
"columnNames": [
"message",
"sequence"
],
"createSql": "CREATE UNIQUE INDEX `index_attachment_message_sequence` ON `${TABLE_NAME}` (`message`, `sequence`)"
}
],
"foreignKeys": [
{
"table": "message",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"message"
],
"referencedColumns": [
"id"
]
}
]
},
{
"tableName": "operation",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `folder` INTEGER NOT NULL, `message` INTEGER NOT NULL, `name` TEXT NOT NULL, `args` TEXT NOT NULL, `created` INTEGER NOT NULL, FOREIGN KEY(`folder`) REFERENCES `folder`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`message`) REFERENCES `message`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "folder",
"columnName": "folder",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "message",
"columnName": "message",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "args",
"columnName": "args",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "created",
"columnName": "created",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": true
},
"indices": [
{
"name": "index_operation_folder",
"unique": false,
"columnNames": [
"folder"
],
"createSql": "CREATE INDEX `index_operation_folder` ON `${TABLE_NAME}` (`folder`)"
},
{
"name": "index_operation_message",
"unique": false,
"columnNames": [
"message"
],
"createSql": "CREATE INDEX `index_operation_message` ON `${TABLE_NAME}` (`message`)"
}
],
"foreignKeys": [
{
"table": "folder",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"folder"
],
"referencedColumns": [
"id"
]
},
{
"table": "message",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"message"
],
"referencedColumns": [
"id"
]
}
]
},
{
"tableName": "answer",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `name` TEXT NOT NULL, `text` TEXT NOT NULL)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "text",
"columnName": "text",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": true
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "log",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `time` INTEGER NOT NULL, `data` TEXT NOT NULL)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "time",
"columnName": "time",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "data",
"columnName": "data",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": true
},
"indices": [
{
"name": "index_log_time",
"unique": false,
"columnNames": [
"time"
],
"createSql": "CREATE INDEX `index_log_time` ON `${TABLE_NAME}` (`time`)"
}
],
"foreignKeys": []
}
],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, \"8a267f8f3cb9ef409377dbdb7cb706d4\")"
]
}
}

View File

@@ -0,0 +1,925 @@
{
"formatVersion": 1,
"database": {
"version": 11,
"identityHash": "d0a6171ec8d9a64a1c65e8c7e5d0348a",
"entities": [
{
"tableName": "identity",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `name` TEXT NOT NULL, `email` TEXT NOT NULL, `replyto` TEXT, `account` INTEGER NOT NULL, `host` TEXT NOT NULL, `port` INTEGER NOT NULL, `starttls` INTEGER NOT NULL, `user` TEXT NOT NULL, `password` TEXT NOT NULL, `auth_type` INTEGER NOT NULL, `primary` INTEGER NOT NULL, `synchronize` INTEGER NOT NULL, `store_sent` INTEGER NOT NULL, `state` TEXT, `error` TEXT, FOREIGN KEY(`account`) REFERENCES `account`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "email",
"columnName": "email",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "replyto",
"columnName": "replyto",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "account",
"columnName": "account",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "host",
"columnName": "host",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "port",
"columnName": "port",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "starttls",
"columnName": "starttls",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "user",
"columnName": "user",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "password",
"columnName": "password",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "auth_type",
"columnName": "auth_type",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "primary",
"columnName": "primary",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "synchronize",
"columnName": "synchronize",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "store_sent",
"columnName": "store_sent",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "state",
"columnName": "state",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "error",
"columnName": "error",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": true
},
"indices": [
{
"name": "index_identity_account",
"unique": false,
"columnNames": [
"account"
],
"createSql": "CREATE INDEX `index_identity_account` ON `${TABLE_NAME}` (`account`)"
}
],
"foreignKeys": [
{
"table": "account",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"account"
],
"referencedColumns": [
"id"
]
}
]
},
{
"tableName": "account",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `name` TEXT, `signature` TEXT, `host` TEXT NOT NULL, `port` INTEGER NOT NULL, `user` TEXT NOT NULL, `password` TEXT NOT NULL, `auth_type` INTEGER NOT NULL, `primary` INTEGER NOT NULL, `synchronize` INTEGER NOT NULL, `store_sent` INTEGER NOT NULL, `poll_interval` INTEGER NOT NULL, `seen_until` INTEGER, `state` TEXT, `error` TEXT)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "signature",
"columnName": "signature",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "host",
"columnName": "host",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "port",
"columnName": "port",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "user",
"columnName": "user",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "password",
"columnName": "password",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "auth_type",
"columnName": "auth_type",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "primary",
"columnName": "primary",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "synchronize",
"columnName": "synchronize",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "store_sent",
"columnName": "store_sent",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "poll_interval",
"columnName": "poll_interval",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "seen_until",
"columnName": "seen_until",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "state",
"columnName": "state",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "error",
"columnName": "error",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": true
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "folder",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `account` INTEGER, `name` TEXT NOT NULL, `type` TEXT NOT NULL, `unified` INTEGER NOT NULL, `synchronize` INTEGER NOT NULL, `after` INTEGER NOT NULL, `state` TEXT, `error` TEXT, FOREIGN KEY(`account`) REFERENCES `account`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "account",
"columnName": "account",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "type",
"columnName": "type",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "unified",
"columnName": "unified",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "synchronize",
"columnName": "synchronize",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "after",
"columnName": "after",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "state",
"columnName": "state",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "error",
"columnName": "error",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": true
},
"indices": [
{
"name": "index_folder_account_name",
"unique": true,
"columnNames": [
"account",
"name"
],
"createSql": "CREATE UNIQUE INDEX `index_folder_account_name` ON `${TABLE_NAME}` (`account`, `name`)"
},
{
"name": "index_folder_account",
"unique": false,
"columnNames": [
"account"
],
"createSql": "CREATE INDEX `index_folder_account` ON `${TABLE_NAME}` (`account`)"
},
{
"name": "index_folder_name",
"unique": false,
"columnNames": [
"name"
],
"createSql": "CREATE INDEX `index_folder_name` ON `${TABLE_NAME}` (`name`)"
},
{
"name": "index_folder_type",
"unique": false,
"columnNames": [
"type"
],
"createSql": "CREATE INDEX `index_folder_type` ON `${TABLE_NAME}` (`type`)"
},
{
"name": "index_folder_unified",
"unique": false,
"columnNames": [
"unified"
],
"createSql": "CREATE INDEX `index_folder_unified` ON `${TABLE_NAME}` (`unified`)"
}
],
"foreignKeys": [
{
"table": "account",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"account"
],
"referencedColumns": [
"id"
]
}
]
},
{
"tableName": "message",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `account` INTEGER, `folder` INTEGER NOT NULL, `identity` INTEGER, `replying` INTEGER, `uid` INTEGER, `msgid` TEXT, `references` TEXT, `inreplyto` TEXT, `thread` TEXT, `from` TEXT, `to` TEXT, `cc` TEXT, `bcc` TEXT, `reply` TEXT, `headers` TEXT, `subject` TEXT, `sent` INTEGER, `received` INTEGER NOT NULL, `stored` INTEGER NOT NULL, `seen` INTEGER NOT NULL, `ui_seen` INTEGER NOT NULL, `ui_hide` INTEGER NOT NULL, `ui_found` INTEGER NOT NULL, `error` TEXT, FOREIGN KEY(`account`) REFERENCES `account`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`folder`) REFERENCES `folder`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`identity`) REFERENCES `identity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`replying`) REFERENCES `message`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "account",
"columnName": "account",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "folder",
"columnName": "folder",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "identity",
"columnName": "identity",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "replying",
"columnName": "replying",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "uid",
"columnName": "uid",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "msgid",
"columnName": "msgid",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "references",
"columnName": "references",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "inreplyto",
"columnName": "inreplyto",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "thread",
"columnName": "thread",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "from",
"columnName": "from",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "to",
"columnName": "to",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "cc",
"columnName": "cc",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "bcc",
"columnName": "bcc",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "reply",
"columnName": "reply",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "headers",
"columnName": "headers",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "subject",
"columnName": "subject",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "sent",
"columnName": "sent",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "received",
"columnName": "received",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "stored",
"columnName": "stored",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "seen",
"columnName": "seen",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "ui_seen",
"columnName": "ui_seen",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "ui_hide",
"columnName": "ui_hide",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "ui_found",
"columnName": "ui_found",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "error",
"columnName": "error",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": true
},
"indices": [
{
"name": "index_message_account",
"unique": false,
"columnNames": [
"account"
],
"createSql": "CREATE INDEX `index_message_account` ON `${TABLE_NAME}` (`account`)"
},
{
"name": "index_message_folder",
"unique": false,
"columnNames": [
"folder"
],
"createSql": "CREATE INDEX `index_message_folder` ON `${TABLE_NAME}` (`folder`)"
},
{
"name": "index_message_identity",
"unique": false,
"columnNames": [
"identity"
],
"createSql": "CREATE INDEX `index_message_identity` ON `${TABLE_NAME}` (`identity`)"
},
{
"name": "index_message_replying",
"unique": false,
"columnNames": [
"replying"
],
"createSql": "CREATE INDEX `index_message_replying` ON `${TABLE_NAME}` (`replying`)"
},
{
"name": "index_message_folder_uid",
"unique": true,
"columnNames": [
"folder",
"uid"
],
"createSql": "CREATE UNIQUE INDEX `index_message_folder_uid` ON `${TABLE_NAME}` (`folder`, `uid`)"
},
{
"name": "index_message_msgid_folder",
"unique": true,
"columnNames": [
"msgid",
"folder"
],
"createSql": "CREATE UNIQUE INDEX `index_message_msgid_folder` ON `${TABLE_NAME}` (`msgid`, `folder`)"
},
{
"name": "index_message_thread",
"unique": false,
"columnNames": [
"thread"
],
"createSql": "CREATE INDEX `index_message_thread` ON `${TABLE_NAME}` (`thread`)"
},
{
"name": "index_message_received",
"unique": false,
"columnNames": [
"received"
],
"createSql": "CREATE INDEX `index_message_received` ON `${TABLE_NAME}` (`received`)"
},
{
"name": "index_message_ui_seen",
"unique": false,
"columnNames": [
"ui_seen"
],
"createSql": "CREATE INDEX `index_message_ui_seen` ON `${TABLE_NAME}` (`ui_seen`)"
},
{
"name": "index_message_ui_hide",
"unique": false,
"columnNames": [
"ui_hide"
],
"createSql": "CREATE INDEX `index_message_ui_hide` ON `${TABLE_NAME}` (`ui_hide`)"
},
{
"name": "index_message_ui_found",
"unique": false,
"columnNames": [
"ui_found"
],
"createSql": "CREATE INDEX `index_message_ui_found` ON `${TABLE_NAME}` (`ui_found`)"
}
],
"foreignKeys": [
{
"table": "account",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"account"
],
"referencedColumns": [
"id"
]
},
{
"table": "folder",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"folder"
],
"referencedColumns": [
"id"
]
},
{
"table": "identity",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"identity"
],
"referencedColumns": [
"id"
]
},
{
"table": "message",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"replying"
],
"referencedColumns": [
"id"
]
}
]
},
{
"tableName": "attachment",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `message` INTEGER NOT NULL, `sequence` INTEGER NOT NULL, `name` TEXT, `type` TEXT NOT NULL, `size` INTEGER, `progress` INTEGER, `available` INTEGER NOT NULL, FOREIGN KEY(`message`) REFERENCES `message`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "message",
"columnName": "message",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "sequence",
"columnName": "sequence",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "type",
"columnName": "type",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "size",
"columnName": "size",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "progress",
"columnName": "progress",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "available",
"columnName": "available",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": true
},
"indices": [
{
"name": "index_attachment_message",
"unique": false,
"columnNames": [
"message"
],
"createSql": "CREATE INDEX `index_attachment_message` ON `${TABLE_NAME}` (`message`)"
},
{
"name": "index_attachment_message_sequence",
"unique": true,
"columnNames": [
"message",
"sequence"
],
"createSql": "CREATE UNIQUE INDEX `index_attachment_message_sequence` ON `${TABLE_NAME}` (`message`, `sequence`)"
}
],
"foreignKeys": [
{
"table": "message",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"message"
],
"referencedColumns": [
"id"
]
}
]
},
{
"tableName": "operation",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `folder` INTEGER NOT NULL, `message` INTEGER NOT NULL, `name` TEXT NOT NULL, `args` TEXT NOT NULL, `created` INTEGER NOT NULL, FOREIGN KEY(`folder`) REFERENCES `folder`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`message`) REFERENCES `message`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "folder",
"columnName": "folder",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "message",
"columnName": "message",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "args",
"columnName": "args",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "created",
"columnName": "created",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": true
},
"indices": [
{
"name": "index_operation_folder",
"unique": false,
"columnNames": [
"folder"
],
"createSql": "CREATE INDEX `index_operation_folder` ON `${TABLE_NAME}` (`folder`)"
},
{
"name": "index_operation_message",
"unique": false,
"columnNames": [
"message"
],
"createSql": "CREATE INDEX `index_operation_message` ON `${TABLE_NAME}` (`message`)"
}
],
"foreignKeys": [
{
"table": "folder",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"folder"
],
"referencedColumns": [
"id"
]
},
{
"table": "message",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"message"
],
"referencedColumns": [
"id"
]
}
]
},
{
"tableName": "answer",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `name` TEXT NOT NULL, `text` TEXT NOT NULL)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "text",
"columnName": "text",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": true
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "log",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `time` INTEGER NOT NULL, `data` TEXT NOT NULL)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "time",
"columnName": "time",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "data",
"columnName": "data",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": true
},
"indices": [
{
"name": "index_log_time",
"unique": false,
"columnNames": [
"time"
],
"createSql": "CREATE INDEX `index_log_time` ON `${TABLE_NAME}` (`time`)"
}
],
"foreignKeys": []
}
],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, \"d0a6171ec8d9a64a1c65e8c7e5d0348a\")"
]
}
}

View File

@@ -0,0 +1,937 @@
{
"formatVersion": 1,
"database": {
"version": 12,
"identityHash": "fe661376a25d2e8b6a9f3e1fa9956daf",
"entities": [
{
"tableName": "identity",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `name` TEXT NOT NULL, `email` TEXT NOT NULL, `replyto` TEXT, `account` INTEGER NOT NULL, `host` TEXT NOT NULL, `port` INTEGER NOT NULL, `starttls` INTEGER NOT NULL, `user` TEXT NOT NULL, `password` TEXT NOT NULL, `auth_type` INTEGER NOT NULL, `primary` INTEGER NOT NULL, `synchronize` INTEGER NOT NULL, `store_sent` INTEGER NOT NULL, `state` TEXT, `error` TEXT, FOREIGN KEY(`account`) REFERENCES `account`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "email",
"columnName": "email",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "replyto",
"columnName": "replyto",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "account",
"columnName": "account",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "host",
"columnName": "host",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "port",
"columnName": "port",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "starttls",
"columnName": "starttls",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "user",
"columnName": "user",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "password",
"columnName": "password",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "auth_type",
"columnName": "auth_type",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "primary",
"columnName": "primary",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "synchronize",
"columnName": "synchronize",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "store_sent",
"columnName": "store_sent",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "state",
"columnName": "state",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "error",
"columnName": "error",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": true
},
"indices": [
{
"name": "index_identity_account",
"unique": false,
"columnNames": [
"account"
],
"createSql": "CREATE INDEX `index_identity_account` ON `${TABLE_NAME}` (`account`)"
}
],
"foreignKeys": [
{
"table": "account",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"account"
],
"referencedColumns": [
"id"
]
}
]
},
{
"tableName": "account",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `name` TEXT, `signature` TEXT, `host` TEXT NOT NULL, `port` INTEGER NOT NULL, `user` TEXT NOT NULL, `password` TEXT NOT NULL, `auth_type` INTEGER NOT NULL, `primary` INTEGER NOT NULL, `synchronize` INTEGER NOT NULL, `store_sent` INTEGER NOT NULL, `poll_interval` INTEGER NOT NULL, `seen_until` INTEGER, `state` TEXT, `error` TEXT)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "signature",
"columnName": "signature",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "host",
"columnName": "host",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "port",
"columnName": "port",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "user",
"columnName": "user",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "password",
"columnName": "password",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "auth_type",
"columnName": "auth_type",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "primary",
"columnName": "primary",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "synchronize",
"columnName": "synchronize",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "store_sent",
"columnName": "store_sent",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "poll_interval",
"columnName": "poll_interval",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "seen_until",
"columnName": "seen_until",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "state",
"columnName": "state",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "error",
"columnName": "error",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": true
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "folder",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `account` INTEGER, `name` TEXT NOT NULL, `type` TEXT NOT NULL, `unified` INTEGER NOT NULL, `synchronize` INTEGER NOT NULL, `after` INTEGER NOT NULL, `state` TEXT, `error` TEXT, FOREIGN KEY(`account`) REFERENCES `account`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "account",
"columnName": "account",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "type",
"columnName": "type",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "unified",
"columnName": "unified",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "synchronize",
"columnName": "synchronize",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "after",
"columnName": "after",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "state",
"columnName": "state",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "error",
"columnName": "error",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": true
},
"indices": [
{
"name": "index_folder_account_name",
"unique": true,
"columnNames": [
"account",
"name"
],
"createSql": "CREATE UNIQUE INDEX `index_folder_account_name` ON `${TABLE_NAME}` (`account`, `name`)"
},
{
"name": "index_folder_account",
"unique": false,
"columnNames": [
"account"
],
"createSql": "CREATE INDEX `index_folder_account` ON `${TABLE_NAME}` (`account`)"
},
{
"name": "index_folder_name",
"unique": false,
"columnNames": [
"name"
],
"createSql": "CREATE INDEX `index_folder_name` ON `${TABLE_NAME}` (`name`)"
},
{
"name": "index_folder_type",
"unique": false,
"columnNames": [
"type"
],
"createSql": "CREATE INDEX `index_folder_type` ON `${TABLE_NAME}` (`type`)"
},
{
"name": "index_folder_unified",
"unique": false,
"columnNames": [
"unified"
],
"createSql": "CREATE INDEX `index_folder_unified` ON `${TABLE_NAME}` (`unified`)"
}
],
"foreignKeys": [
{
"table": "account",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"account"
],
"referencedColumns": [
"id"
]
}
]
},
{
"tableName": "message",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `account` INTEGER, `folder` INTEGER NOT NULL, `identity` INTEGER, `replying` INTEGER, `uid` INTEGER, `msgid` TEXT, `references` TEXT, `inreplyto` TEXT, `thread` TEXT, `from` TEXT, `to` TEXT, `cc` TEXT, `bcc` TEXT, `reply` TEXT, `headers` TEXT, `subject` TEXT, `sent` INTEGER, `received` INTEGER NOT NULL, `stored` INTEGER NOT NULL, `seen` INTEGER NOT NULL, `flagged` INTEGER NOT NULL, `ui_seen` INTEGER NOT NULL, `ui_flagged` INTEGER NOT NULL, `ui_hide` INTEGER NOT NULL, `ui_found` INTEGER NOT NULL, `error` TEXT, FOREIGN KEY(`account`) REFERENCES `account`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`folder`) REFERENCES `folder`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`identity`) REFERENCES `identity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`replying`) REFERENCES `message`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "account",
"columnName": "account",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "folder",
"columnName": "folder",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "identity",
"columnName": "identity",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "replying",
"columnName": "replying",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "uid",
"columnName": "uid",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "msgid",
"columnName": "msgid",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "references",
"columnName": "references",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "inreplyto",
"columnName": "inreplyto",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "thread",
"columnName": "thread",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "from",
"columnName": "from",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "to",
"columnName": "to",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "cc",
"columnName": "cc",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "bcc",
"columnName": "bcc",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "reply",
"columnName": "reply",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "headers",
"columnName": "headers",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "subject",
"columnName": "subject",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "sent",
"columnName": "sent",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "received",
"columnName": "received",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "stored",
"columnName": "stored",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "seen",
"columnName": "seen",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "flagged",
"columnName": "flagged",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "ui_seen",
"columnName": "ui_seen",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "ui_flagged",
"columnName": "ui_flagged",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "ui_hide",
"columnName": "ui_hide",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "ui_found",
"columnName": "ui_found",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "error",
"columnName": "error",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": true
},
"indices": [
{
"name": "index_message_account",
"unique": false,
"columnNames": [
"account"
],
"createSql": "CREATE INDEX `index_message_account` ON `${TABLE_NAME}` (`account`)"
},
{
"name": "index_message_folder",
"unique": false,
"columnNames": [
"folder"
],
"createSql": "CREATE INDEX `index_message_folder` ON `${TABLE_NAME}` (`folder`)"
},
{
"name": "index_message_identity",
"unique": false,
"columnNames": [
"identity"
],
"createSql": "CREATE INDEX `index_message_identity` ON `${TABLE_NAME}` (`identity`)"
},
{
"name": "index_message_replying",
"unique": false,
"columnNames": [
"replying"
],
"createSql": "CREATE INDEX `index_message_replying` ON `${TABLE_NAME}` (`replying`)"
},
{
"name": "index_message_folder_uid",
"unique": true,
"columnNames": [
"folder",
"uid"
],
"createSql": "CREATE UNIQUE INDEX `index_message_folder_uid` ON `${TABLE_NAME}` (`folder`, `uid`)"
},
{
"name": "index_message_msgid_folder",
"unique": true,
"columnNames": [
"msgid",
"folder"
],
"createSql": "CREATE UNIQUE INDEX `index_message_msgid_folder` ON `${TABLE_NAME}` (`msgid`, `folder`)"
},
{
"name": "index_message_thread",
"unique": false,
"columnNames": [
"thread"
],
"createSql": "CREATE INDEX `index_message_thread` ON `${TABLE_NAME}` (`thread`)"
},
{
"name": "index_message_received",
"unique": false,
"columnNames": [
"received"
],
"createSql": "CREATE INDEX `index_message_received` ON `${TABLE_NAME}` (`received`)"
},
{
"name": "index_message_ui_seen",
"unique": false,
"columnNames": [
"ui_seen"
],
"createSql": "CREATE INDEX `index_message_ui_seen` ON `${TABLE_NAME}` (`ui_seen`)"
},
{
"name": "index_message_ui_hide",
"unique": false,
"columnNames": [
"ui_hide"
],
"createSql": "CREATE INDEX `index_message_ui_hide` ON `${TABLE_NAME}` (`ui_hide`)"
},
{
"name": "index_message_ui_found",
"unique": false,
"columnNames": [
"ui_found"
],
"createSql": "CREATE INDEX `index_message_ui_found` ON `${TABLE_NAME}` (`ui_found`)"
}
],
"foreignKeys": [
{
"table": "account",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"account"
],
"referencedColumns": [
"id"
]
},
{
"table": "folder",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"folder"
],
"referencedColumns": [
"id"
]
},
{
"table": "identity",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"identity"
],
"referencedColumns": [
"id"
]
},
{
"table": "message",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"replying"
],
"referencedColumns": [
"id"
]
}
]
},
{
"tableName": "attachment",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `message` INTEGER NOT NULL, `sequence` INTEGER NOT NULL, `name` TEXT, `type` TEXT NOT NULL, `size` INTEGER, `progress` INTEGER, `available` INTEGER NOT NULL, FOREIGN KEY(`message`) REFERENCES `message`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "message",
"columnName": "message",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "sequence",
"columnName": "sequence",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "type",
"columnName": "type",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "size",
"columnName": "size",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "progress",
"columnName": "progress",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "available",
"columnName": "available",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": true
},
"indices": [
{
"name": "index_attachment_message",
"unique": false,
"columnNames": [
"message"
],
"createSql": "CREATE INDEX `index_attachment_message` ON `${TABLE_NAME}` (`message`)"
},
{
"name": "index_attachment_message_sequence",
"unique": true,
"columnNames": [
"message",
"sequence"
],
"createSql": "CREATE UNIQUE INDEX `index_attachment_message_sequence` ON `${TABLE_NAME}` (`message`, `sequence`)"
}
],
"foreignKeys": [
{
"table": "message",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"message"
],
"referencedColumns": [
"id"
]
}
]
},
{
"tableName": "operation",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `folder` INTEGER NOT NULL, `message` INTEGER NOT NULL, `name` TEXT NOT NULL, `args` TEXT NOT NULL, `created` INTEGER NOT NULL, FOREIGN KEY(`folder`) REFERENCES `folder`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`message`) REFERENCES `message`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "folder",
"columnName": "folder",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "message",
"columnName": "message",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "args",
"columnName": "args",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "created",
"columnName": "created",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": true
},
"indices": [
{
"name": "index_operation_folder",
"unique": false,
"columnNames": [
"folder"
],
"createSql": "CREATE INDEX `index_operation_folder` ON `${TABLE_NAME}` (`folder`)"
},
{
"name": "index_operation_message",
"unique": false,
"columnNames": [
"message"
],
"createSql": "CREATE INDEX `index_operation_message` ON `${TABLE_NAME}` (`message`)"
}
],
"foreignKeys": [
{
"table": "folder",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"folder"
],
"referencedColumns": [
"id"
]
},
{
"table": "message",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"message"
],
"referencedColumns": [
"id"
]
}
]
},
{
"tableName": "answer",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `name` TEXT NOT NULL, `text` TEXT NOT NULL)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "text",
"columnName": "text",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": true
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "log",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `time` INTEGER NOT NULL, `data` TEXT NOT NULL)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "time",
"columnName": "time",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "data",
"columnName": "data",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": true
},
"indices": [
{
"name": "index_log_time",
"unique": false,
"columnNames": [
"time"
],
"createSql": "CREATE INDEX `index_log_time` ON `${TABLE_NAME}` (`time`)"
}
],
"foreignKeys": []
}
],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, \"fe661376a25d2e8b6a9f3e1fa9956daf\")"
]
}
}

View File

@@ -0,0 +1,850 @@
{
"formatVersion": 1,
"database": {
"version": 6,
"identityHash": "e9ae946be1049502f01f8f30275abc2f",
"entities": [
{
"tableName": "identity",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `name` TEXT NOT NULL, `email` TEXT NOT NULL, `replyto` TEXT, `account` INTEGER NOT NULL, `host` TEXT NOT NULL, `port` INTEGER NOT NULL, `starttls` INTEGER NOT NULL, `user` TEXT NOT NULL, `password` TEXT NOT NULL, `auth_type` INTEGER NOT NULL, `primary` INTEGER NOT NULL, `synchronize` INTEGER NOT NULL, `store_sent` INTEGER NOT NULL, `state` TEXT, `error` TEXT, FOREIGN KEY(`account`) REFERENCES `account`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "email",
"columnName": "email",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "replyto",
"columnName": "replyto",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "account",
"columnName": "account",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "host",
"columnName": "host",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "port",
"columnName": "port",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "starttls",
"columnName": "starttls",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "user",
"columnName": "user",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "password",
"columnName": "password",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "auth_type",
"columnName": "auth_type",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "primary",
"columnName": "primary",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "synchronize",
"columnName": "synchronize",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "store_sent",
"columnName": "store_sent",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "state",
"columnName": "state",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "error",
"columnName": "error",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": true
},
"indices": [
{
"name": "index_identity_account",
"unique": false,
"columnNames": [
"account"
],
"createSql": "CREATE INDEX `index_identity_account` ON `${TABLE_NAME}` (`account`)"
}
],
"foreignKeys": [
{
"table": "account",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"account"
],
"referencedColumns": [
"id"
]
}
]
},
{
"tableName": "account",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `name` TEXT, `host` TEXT NOT NULL, `port` INTEGER NOT NULL, `user` TEXT NOT NULL, `password` TEXT NOT NULL, `auth_type` INTEGER NOT NULL, `primary` INTEGER NOT NULL, `synchronize` INTEGER NOT NULL, `store_sent` INTEGER NOT NULL, `poll_interval` INTEGER NOT NULL, `seen_until` INTEGER, `state` TEXT, `error` TEXT)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "host",
"columnName": "host",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "port",
"columnName": "port",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "user",
"columnName": "user",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "password",
"columnName": "password",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "auth_type",
"columnName": "auth_type",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "primary",
"columnName": "primary",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "synchronize",
"columnName": "synchronize",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "store_sent",
"columnName": "store_sent",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "poll_interval",
"columnName": "poll_interval",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "seen_until",
"columnName": "seen_until",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "state",
"columnName": "state",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "error",
"columnName": "error",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": true
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "folder",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `account` INTEGER, `name` TEXT NOT NULL, `type` TEXT NOT NULL, `synchronize` INTEGER NOT NULL, `after` INTEGER NOT NULL, `state` TEXT, `error` TEXT, FOREIGN KEY(`account`) REFERENCES `account`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "account",
"columnName": "account",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "type",
"columnName": "type",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "synchronize",
"columnName": "synchronize",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "after",
"columnName": "after",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "state",
"columnName": "state",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "error",
"columnName": "error",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": true
},
"indices": [
{
"name": "index_folder_account_name",
"unique": true,
"columnNames": [
"account",
"name"
],
"createSql": "CREATE UNIQUE INDEX `index_folder_account_name` ON `${TABLE_NAME}` (`account`, `name`)"
},
{
"name": "index_folder_account",
"unique": false,
"columnNames": [
"account"
],
"createSql": "CREATE INDEX `index_folder_account` ON `${TABLE_NAME}` (`account`)"
},
{
"name": "index_folder_name",
"unique": false,
"columnNames": [
"name"
],
"createSql": "CREATE INDEX `index_folder_name` ON `${TABLE_NAME}` (`name`)"
},
{
"name": "index_folder_type",
"unique": false,
"columnNames": [
"type"
],
"createSql": "CREATE INDEX `index_folder_type` ON `${TABLE_NAME}` (`type`)"
}
],
"foreignKeys": [
{
"table": "account",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"account"
],
"referencedColumns": [
"id"
]
}
]
},
{
"tableName": "message",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `account` INTEGER, `folder` INTEGER NOT NULL, `identity` INTEGER, `replying` INTEGER, `uid` INTEGER, `msgid` TEXT, `references` TEXT, `inreplyto` TEXT, `thread` TEXT, `from` TEXT, `to` TEXT, `cc` TEXT, `bcc` TEXT, `reply` TEXT, `subject` TEXT, `sent` INTEGER, `received` INTEGER NOT NULL, `stored` INTEGER NOT NULL, `seen` INTEGER NOT NULL, `ui_seen` INTEGER NOT NULL, `ui_hide` INTEGER NOT NULL, `ui_found` INTEGER NOT NULL, `error` TEXT, FOREIGN KEY(`account`) REFERENCES `account`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`folder`) REFERENCES `folder`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`identity`) REFERENCES `identity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`replying`) REFERENCES `message`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "account",
"columnName": "account",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "folder",
"columnName": "folder",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "identity",
"columnName": "identity",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "replying",
"columnName": "replying",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "uid",
"columnName": "uid",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "msgid",
"columnName": "msgid",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "references",
"columnName": "references",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "inreplyto",
"columnName": "inreplyto",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "thread",
"columnName": "thread",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "from",
"columnName": "from",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "to",
"columnName": "to",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "cc",
"columnName": "cc",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "bcc",
"columnName": "bcc",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "reply",
"columnName": "reply",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "subject",
"columnName": "subject",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "sent",
"columnName": "sent",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "received",
"columnName": "received",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "stored",
"columnName": "stored",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "seen",
"columnName": "seen",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "ui_seen",
"columnName": "ui_seen",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "ui_hide",
"columnName": "ui_hide",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "ui_found",
"columnName": "ui_found",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "error",
"columnName": "error",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": true
},
"indices": [
{
"name": "index_message_account",
"unique": false,
"columnNames": [
"account"
],
"createSql": "CREATE INDEX `index_message_account` ON `${TABLE_NAME}` (`account`)"
},
{
"name": "index_message_folder",
"unique": false,
"columnNames": [
"folder"
],
"createSql": "CREATE INDEX `index_message_folder` ON `${TABLE_NAME}` (`folder`)"
},
{
"name": "index_message_identity",
"unique": false,
"columnNames": [
"identity"
],
"createSql": "CREATE INDEX `index_message_identity` ON `${TABLE_NAME}` (`identity`)"
},
{
"name": "index_message_replying",
"unique": false,
"columnNames": [
"replying"
],
"createSql": "CREATE INDEX `index_message_replying` ON `${TABLE_NAME}` (`replying`)"
},
{
"name": "index_message_folder_uid",
"unique": true,
"columnNames": [
"folder",
"uid"
],
"createSql": "CREATE UNIQUE INDEX `index_message_folder_uid` ON `${TABLE_NAME}` (`folder`, `uid`)"
},
{
"name": "index_message_msgid_folder",
"unique": true,
"columnNames": [
"msgid",
"folder"
],
"createSql": "CREATE UNIQUE INDEX `index_message_msgid_folder` ON `${TABLE_NAME}` (`msgid`, `folder`)"
},
{
"name": "index_message_thread",
"unique": false,
"columnNames": [
"thread"
],
"createSql": "CREATE INDEX `index_message_thread` ON `${TABLE_NAME}` (`thread`)"
},
{
"name": "index_message_received",
"unique": false,
"columnNames": [
"received"
],
"createSql": "CREATE INDEX `index_message_received` ON `${TABLE_NAME}` (`received`)"
},
{
"name": "index_message_ui_seen",
"unique": false,
"columnNames": [
"ui_seen"
],
"createSql": "CREATE INDEX `index_message_ui_seen` ON `${TABLE_NAME}` (`ui_seen`)"
},
{
"name": "index_message_ui_hide",
"unique": false,
"columnNames": [
"ui_hide"
],
"createSql": "CREATE INDEX `index_message_ui_hide` ON `${TABLE_NAME}` (`ui_hide`)"
}
],
"foreignKeys": [
{
"table": "account",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"account"
],
"referencedColumns": [
"id"
]
},
{
"table": "folder",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"folder"
],
"referencedColumns": [
"id"
]
},
{
"table": "identity",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"identity"
],
"referencedColumns": [
"id"
]
},
{
"table": "message",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"replying"
],
"referencedColumns": [
"id"
]
}
]
},
{
"tableName": "attachment",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `message` INTEGER NOT NULL, `sequence` INTEGER NOT NULL, `name` TEXT, `type` TEXT NOT NULL, `size` INTEGER, `progress` INTEGER, `available` INTEGER NOT NULL, FOREIGN KEY(`message`) REFERENCES `message`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "message",
"columnName": "message",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "sequence",
"columnName": "sequence",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "type",
"columnName": "type",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "size",
"columnName": "size",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "progress",
"columnName": "progress",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "available",
"columnName": "available",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": true
},
"indices": [
{
"name": "index_attachment_message",
"unique": false,
"columnNames": [
"message"
],
"createSql": "CREATE INDEX `index_attachment_message` ON `${TABLE_NAME}` (`message`)"
},
{
"name": "index_attachment_message_sequence",
"unique": true,
"columnNames": [
"message",
"sequence"
],
"createSql": "CREATE UNIQUE INDEX `index_attachment_message_sequence` ON `${TABLE_NAME}` (`message`, `sequence`)"
}
],
"foreignKeys": [
{
"table": "message",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"message"
],
"referencedColumns": [
"id"
]
}
]
},
{
"tableName": "operation",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `folder` INTEGER NOT NULL, `message` INTEGER NOT NULL, `name` TEXT NOT NULL, `args` TEXT NOT NULL, `created` INTEGER NOT NULL, FOREIGN KEY(`folder`) REFERENCES `folder`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`message`) REFERENCES `message`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "folder",
"columnName": "folder",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "message",
"columnName": "message",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "args",
"columnName": "args",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "created",
"columnName": "created",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": true
},
"indices": [
{
"name": "index_operation_folder",
"unique": false,
"columnNames": [
"folder"
],
"createSql": "CREATE INDEX `index_operation_folder` ON `${TABLE_NAME}` (`folder`)"
},
{
"name": "index_operation_message",
"unique": false,
"columnNames": [
"message"
],
"createSql": "CREATE INDEX `index_operation_message` ON `${TABLE_NAME}` (`message`)"
}
],
"foreignKeys": [
{
"table": "folder",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"folder"
],
"referencedColumns": [
"id"
]
},
{
"table": "message",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"message"
],
"referencedColumns": [
"id"
]
}
]
},
{
"tableName": "answer",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `name` TEXT NOT NULL, `text` TEXT NOT NULL)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "text",
"columnName": "text",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": true
},
"indices": [],
"foreignKeys": []
}
],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, \"e9ae946be1049502f01f8f30275abc2f\")"
]
}
}

View File

@@ -0,0 +1,891 @@
{
"formatVersion": 1,
"database": {
"version": 7,
"identityHash": "78658430615109b7c163e62ed1ad0a48",
"entities": [
{
"tableName": "identity",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `name` TEXT NOT NULL, `email` TEXT NOT NULL, `replyto` TEXT, `account` INTEGER NOT NULL, `host` TEXT NOT NULL, `port` INTEGER NOT NULL, `starttls` INTEGER NOT NULL, `user` TEXT NOT NULL, `password` TEXT NOT NULL, `auth_type` INTEGER NOT NULL, `primary` INTEGER NOT NULL, `synchronize` INTEGER NOT NULL, `store_sent` INTEGER NOT NULL, `state` TEXT, `error` TEXT, FOREIGN KEY(`account`) REFERENCES `account`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "email",
"columnName": "email",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "replyto",
"columnName": "replyto",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "account",
"columnName": "account",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "host",
"columnName": "host",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "port",
"columnName": "port",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "starttls",
"columnName": "starttls",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "user",
"columnName": "user",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "password",
"columnName": "password",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "auth_type",
"columnName": "auth_type",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "primary",
"columnName": "primary",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "synchronize",
"columnName": "synchronize",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "store_sent",
"columnName": "store_sent",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "state",
"columnName": "state",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "error",
"columnName": "error",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": true
},
"indices": [
{
"name": "index_identity_account",
"unique": false,
"columnNames": [
"account"
],
"createSql": "CREATE INDEX `index_identity_account` ON `${TABLE_NAME}` (`account`)"
}
],
"foreignKeys": [
{
"table": "account",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"account"
],
"referencedColumns": [
"id"
]
}
]
},
{
"tableName": "account",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `name` TEXT, `host` TEXT NOT NULL, `port` INTEGER NOT NULL, `user` TEXT NOT NULL, `password` TEXT NOT NULL, `auth_type` INTEGER NOT NULL, `primary` INTEGER NOT NULL, `synchronize` INTEGER NOT NULL, `store_sent` INTEGER NOT NULL, `poll_interval` INTEGER NOT NULL, `seen_until` INTEGER, `state` TEXT, `error` TEXT)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "host",
"columnName": "host",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "port",
"columnName": "port",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "user",
"columnName": "user",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "password",
"columnName": "password",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "auth_type",
"columnName": "auth_type",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "primary",
"columnName": "primary",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "synchronize",
"columnName": "synchronize",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "store_sent",
"columnName": "store_sent",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "poll_interval",
"columnName": "poll_interval",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "seen_until",
"columnName": "seen_until",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "state",
"columnName": "state",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "error",
"columnName": "error",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": true
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "folder",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `account` INTEGER, `name` TEXT NOT NULL, `type` TEXT NOT NULL, `synchronize` INTEGER NOT NULL, `after` INTEGER NOT NULL, `state` TEXT, `error` TEXT, FOREIGN KEY(`account`) REFERENCES `account`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "account",
"columnName": "account",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "type",
"columnName": "type",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "synchronize",
"columnName": "synchronize",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "after",
"columnName": "after",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "state",
"columnName": "state",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "error",
"columnName": "error",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": true
},
"indices": [
{
"name": "index_folder_account_name",
"unique": true,
"columnNames": [
"account",
"name"
],
"createSql": "CREATE UNIQUE INDEX `index_folder_account_name` ON `${TABLE_NAME}` (`account`, `name`)"
},
{
"name": "index_folder_account",
"unique": false,
"columnNames": [
"account"
],
"createSql": "CREATE INDEX `index_folder_account` ON `${TABLE_NAME}` (`account`)"
},
{
"name": "index_folder_name",
"unique": false,
"columnNames": [
"name"
],
"createSql": "CREATE INDEX `index_folder_name` ON `${TABLE_NAME}` (`name`)"
},
{
"name": "index_folder_type",
"unique": false,
"columnNames": [
"type"
],
"createSql": "CREATE INDEX `index_folder_type` ON `${TABLE_NAME}` (`type`)"
}
],
"foreignKeys": [
{
"table": "account",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"account"
],
"referencedColumns": [
"id"
]
}
]
},
{
"tableName": "message",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `account` INTEGER, `folder` INTEGER NOT NULL, `identity` INTEGER, `replying` INTEGER, `uid` INTEGER, `msgid` TEXT, `references` TEXT, `inreplyto` TEXT, `thread` TEXT, `from` TEXT, `to` TEXT, `cc` TEXT, `bcc` TEXT, `reply` TEXT, `subject` TEXT, `sent` INTEGER, `received` INTEGER NOT NULL, `stored` INTEGER NOT NULL, `seen` INTEGER NOT NULL, `ui_seen` INTEGER NOT NULL, `ui_hide` INTEGER NOT NULL, `ui_found` INTEGER NOT NULL, `error` TEXT, FOREIGN KEY(`account`) REFERENCES `account`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`folder`) REFERENCES `folder`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`identity`) REFERENCES `identity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`replying`) REFERENCES `message`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "account",
"columnName": "account",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "folder",
"columnName": "folder",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "identity",
"columnName": "identity",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "replying",
"columnName": "replying",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "uid",
"columnName": "uid",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "msgid",
"columnName": "msgid",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "references",
"columnName": "references",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "inreplyto",
"columnName": "inreplyto",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "thread",
"columnName": "thread",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "from",
"columnName": "from",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "to",
"columnName": "to",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "cc",
"columnName": "cc",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "bcc",
"columnName": "bcc",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "reply",
"columnName": "reply",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "subject",
"columnName": "subject",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "sent",
"columnName": "sent",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "received",
"columnName": "received",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "stored",
"columnName": "stored",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "seen",
"columnName": "seen",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "ui_seen",
"columnName": "ui_seen",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "ui_hide",
"columnName": "ui_hide",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "ui_found",
"columnName": "ui_found",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "error",
"columnName": "error",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": true
},
"indices": [
{
"name": "index_message_account",
"unique": false,
"columnNames": [
"account"
],
"createSql": "CREATE INDEX `index_message_account` ON `${TABLE_NAME}` (`account`)"
},
{
"name": "index_message_folder",
"unique": false,
"columnNames": [
"folder"
],
"createSql": "CREATE INDEX `index_message_folder` ON `${TABLE_NAME}` (`folder`)"
},
{
"name": "index_message_identity",
"unique": false,
"columnNames": [
"identity"
],
"createSql": "CREATE INDEX `index_message_identity` ON `${TABLE_NAME}` (`identity`)"
},
{
"name": "index_message_replying",
"unique": false,
"columnNames": [
"replying"
],
"createSql": "CREATE INDEX `index_message_replying` ON `${TABLE_NAME}` (`replying`)"
},
{
"name": "index_message_folder_uid",
"unique": true,
"columnNames": [
"folder",
"uid"
],
"createSql": "CREATE UNIQUE INDEX `index_message_folder_uid` ON `${TABLE_NAME}` (`folder`, `uid`)"
},
{
"name": "index_message_msgid_folder",
"unique": true,
"columnNames": [
"msgid",
"folder"
],
"createSql": "CREATE UNIQUE INDEX `index_message_msgid_folder` ON `${TABLE_NAME}` (`msgid`, `folder`)"
},
{
"name": "index_message_thread",
"unique": false,
"columnNames": [
"thread"
],
"createSql": "CREATE INDEX `index_message_thread` ON `${TABLE_NAME}` (`thread`)"
},
{
"name": "index_message_received",
"unique": false,
"columnNames": [
"received"
],
"createSql": "CREATE INDEX `index_message_received` ON `${TABLE_NAME}` (`received`)"
},
{
"name": "index_message_ui_seen",
"unique": false,
"columnNames": [
"ui_seen"
],
"createSql": "CREATE INDEX `index_message_ui_seen` ON `${TABLE_NAME}` (`ui_seen`)"
},
{
"name": "index_message_ui_hide",
"unique": false,
"columnNames": [
"ui_hide"
],
"createSql": "CREATE INDEX `index_message_ui_hide` ON `${TABLE_NAME}` (`ui_hide`)"
}
],
"foreignKeys": [
{
"table": "account",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"account"
],
"referencedColumns": [
"id"
]
},
{
"table": "folder",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"folder"
],
"referencedColumns": [
"id"
]
},
{
"table": "identity",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"identity"
],
"referencedColumns": [
"id"
]
},
{
"table": "message",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"replying"
],
"referencedColumns": [
"id"
]
}
]
},
{
"tableName": "attachment",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `message` INTEGER NOT NULL, `sequence` INTEGER NOT NULL, `name` TEXT, `type` TEXT NOT NULL, `size` INTEGER, `progress` INTEGER, `available` INTEGER NOT NULL, FOREIGN KEY(`message`) REFERENCES `message`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "message",
"columnName": "message",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "sequence",
"columnName": "sequence",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "type",
"columnName": "type",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "size",
"columnName": "size",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "progress",
"columnName": "progress",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "available",
"columnName": "available",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": true
},
"indices": [
{
"name": "index_attachment_message",
"unique": false,
"columnNames": [
"message"
],
"createSql": "CREATE INDEX `index_attachment_message` ON `${TABLE_NAME}` (`message`)"
},
{
"name": "index_attachment_message_sequence",
"unique": true,
"columnNames": [
"message",
"sequence"
],
"createSql": "CREATE UNIQUE INDEX `index_attachment_message_sequence` ON `${TABLE_NAME}` (`message`, `sequence`)"
}
],
"foreignKeys": [
{
"table": "message",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"message"
],
"referencedColumns": [
"id"
]
}
]
},
{
"tableName": "operation",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `folder` INTEGER NOT NULL, `message` INTEGER NOT NULL, `name` TEXT NOT NULL, `args` TEXT NOT NULL, `created` INTEGER NOT NULL, FOREIGN KEY(`folder`) REFERENCES `folder`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`message`) REFERENCES `message`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "folder",
"columnName": "folder",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "message",
"columnName": "message",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "args",
"columnName": "args",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "created",
"columnName": "created",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": true
},
"indices": [
{
"name": "index_operation_folder",
"unique": false,
"columnNames": [
"folder"
],
"createSql": "CREATE INDEX `index_operation_folder` ON `${TABLE_NAME}` (`folder`)"
},
{
"name": "index_operation_message",
"unique": false,
"columnNames": [
"message"
],
"createSql": "CREATE INDEX `index_operation_message` ON `${TABLE_NAME}` (`message`)"
}
],
"foreignKeys": [
{
"table": "folder",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"folder"
],
"referencedColumns": [
"id"
]
},
{
"table": "message",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"message"
],
"referencedColumns": [
"id"
]
}
]
},
{
"tableName": "answer",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `name` TEXT NOT NULL, `text` TEXT NOT NULL)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "text",
"columnName": "text",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": true
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "log",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `time` INTEGER NOT NULL, `data` TEXT NOT NULL)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "time",
"columnName": "time",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "data",
"columnName": "data",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": true
},
"indices": [
{
"name": "index_log_time",
"unique": false,
"columnNames": [
"time"
],
"createSql": "CREATE INDEX `index_log_time` ON `${TABLE_NAME}` (`time`)"
}
],
"foreignKeys": []
}
],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, \"78658430615109b7c163e62ed1ad0a48\")"
]
}
}

View File

@@ -0,0 +1,899 @@
{
"formatVersion": 1,
"database": {
"version": 8,
"identityHash": "6127ad940456ed43d7551f7cd7b7ed18",
"entities": [
{
"tableName": "identity",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `name` TEXT NOT NULL, `email` TEXT NOT NULL, `replyto` TEXT, `account` INTEGER NOT NULL, `host` TEXT NOT NULL, `port` INTEGER NOT NULL, `starttls` INTEGER NOT NULL, `user` TEXT NOT NULL, `password` TEXT NOT NULL, `auth_type` INTEGER NOT NULL, `primary` INTEGER NOT NULL, `synchronize` INTEGER NOT NULL, `store_sent` INTEGER NOT NULL, `state` TEXT, `error` TEXT, FOREIGN KEY(`account`) REFERENCES `account`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "email",
"columnName": "email",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "replyto",
"columnName": "replyto",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "account",
"columnName": "account",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "host",
"columnName": "host",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "port",
"columnName": "port",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "starttls",
"columnName": "starttls",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "user",
"columnName": "user",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "password",
"columnName": "password",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "auth_type",
"columnName": "auth_type",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "primary",
"columnName": "primary",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "synchronize",
"columnName": "synchronize",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "store_sent",
"columnName": "store_sent",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "state",
"columnName": "state",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "error",
"columnName": "error",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": true
},
"indices": [
{
"name": "index_identity_account",
"unique": false,
"columnNames": [
"account"
],
"createSql": "CREATE INDEX `index_identity_account` ON `${TABLE_NAME}` (`account`)"
}
],
"foreignKeys": [
{
"table": "account",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"account"
],
"referencedColumns": [
"id"
]
}
]
},
{
"tableName": "account",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `name` TEXT, `host` TEXT NOT NULL, `port` INTEGER NOT NULL, `user` TEXT NOT NULL, `password` TEXT NOT NULL, `auth_type` INTEGER NOT NULL, `primary` INTEGER NOT NULL, `synchronize` INTEGER NOT NULL, `store_sent` INTEGER NOT NULL, `poll_interval` INTEGER NOT NULL, `seen_until` INTEGER, `state` TEXT, `error` TEXT)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "host",
"columnName": "host",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "port",
"columnName": "port",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "user",
"columnName": "user",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "password",
"columnName": "password",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "auth_type",
"columnName": "auth_type",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "primary",
"columnName": "primary",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "synchronize",
"columnName": "synchronize",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "store_sent",
"columnName": "store_sent",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "poll_interval",
"columnName": "poll_interval",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "seen_until",
"columnName": "seen_until",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "state",
"columnName": "state",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "error",
"columnName": "error",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": true
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "folder",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `account` INTEGER, `name` TEXT NOT NULL, `type` TEXT NOT NULL, `synchronize` INTEGER NOT NULL, `after` INTEGER NOT NULL, `state` TEXT, `error` TEXT, FOREIGN KEY(`account`) REFERENCES `account`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "account",
"columnName": "account",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "type",
"columnName": "type",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "synchronize",
"columnName": "synchronize",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "after",
"columnName": "after",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "state",
"columnName": "state",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "error",
"columnName": "error",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": true
},
"indices": [
{
"name": "index_folder_account_name",
"unique": true,
"columnNames": [
"account",
"name"
],
"createSql": "CREATE UNIQUE INDEX `index_folder_account_name` ON `${TABLE_NAME}` (`account`, `name`)"
},
{
"name": "index_folder_account",
"unique": false,
"columnNames": [
"account"
],
"createSql": "CREATE INDEX `index_folder_account` ON `${TABLE_NAME}` (`account`)"
},
{
"name": "index_folder_name",
"unique": false,
"columnNames": [
"name"
],
"createSql": "CREATE INDEX `index_folder_name` ON `${TABLE_NAME}` (`name`)"
},
{
"name": "index_folder_type",
"unique": false,
"columnNames": [
"type"
],
"createSql": "CREATE INDEX `index_folder_type` ON `${TABLE_NAME}` (`type`)"
}
],
"foreignKeys": [
{
"table": "account",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"account"
],
"referencedColumns": [
"id"
]
}
]
},
{
"tableName": "message",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `account` INTEGER, `folder` INTEGER NOT NULL, `identity` INTEGER, `replying` INTEGER, `uid` INTEGER, `msgid` TEXT, `references` TEXT, `inreplyto` TEXT, `thread` TEXT, `from` TEXT, `to` TEXT, `cc` TEXT, `bcc` TEXT, `reply` TEXT, `subject` TEXT, `sent` INTEGER, `received` INTEGER NOT NULL, `stored` INTEGER NOT NULL, `seen` INTEGER NOT NULL, `ui_seen` INTEGER NOT NULL, `ui_hide` INTEGER NOT NULL, `ui_found` INTEGER NOT NULL, `error` TEXT, FOREIGN KEY(`account`) REFERENCES `account`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`folder`) REFERENCES `folder`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`identity`) REFERENCES `identity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`replying`) REFERENCES `message`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "account",
"columnName": "account",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "folder",
"columnName": "folder",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "identity",
"columnName": "identity",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "replying",
"columnName": "replying",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "uid",
"columnName": "uid",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "msgid",
"columnName": "msgid",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "references",
"columnName": "references",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "inreplyto",
"columnName": "inreplyto",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "thread",
"columnName": "thread",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "from",
"columnName": "from",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "to",
"columnName": "to",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "cc",
"columnName": "cc",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "bcc",
"columnName": "bcc",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "reply",
"columnName": "reply",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "subject",
"columnName": "subject",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "sent",
"columnName": "sent",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "received",
"columnName": "received",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "stored",
"columnName": "stored",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "seen",
"columnName": "seen",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "ui_seen",
"columnName": "ui_seen",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "ui_hide",
"columnName": "ui_hide",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "ui_found",
"columnName": "ui_found",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "error",
"columnName": "error",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": true
},
"indices": [
{
"name": "index_message_account",
"unique": false,
"columnNames": [
"account"
],
"createSql": "CREATE INDEX `index_message_account` ON `${TABLE_NAME}` (`account`)"
},
{
"name": "index_message_folder",
"unique": false,
"columnNames": [
"folder"
],
"createSql": "CREATE INDEX `index_message_folder` ON `${TABLE_NAME}` (`folder`)"
},
{
"name": "index_message_identity",
"unique": false,
"columnNames": [
"identity"
],
"createSql": "CREATE INDEX `index_message_identity` ON `${TABLE_NAME}` (`identity`)"
},
{
"name": "index_message_replying",
"unique": false,
"columnNames": [
"replying"
],
"createSql": "CREATE INDEX `index_message_replying` ON `${TABLE_NAME}` (`replying`)"
},
{
"name": "index_message_folder_uid",
"unique": true,
"columnNames": [
"folder",
"uid"
],
"createSql": "CREATE UNIQUE INDEX `index_message_folder_uid` ON `${TABLE_NAME}` (`folder`, `uid`)"
},
{
"name": "index_message_msgid_folder",
"unique": true,
"columnNames": [
"msgid",
"folder"
],
"createSql": "CREATE UNIQUE INDEX `index_message_msgid_folder` ON `${TABLE_NAME}` (`msgid`, `folder`)"
},
{
"name": "index_message_thread",
"unique": false,
"columnNames": [
"thread"
],
"createSql": "CREATE INDEX `index_message_thread` ON `${TABLE_NAME}` (`thread`)"
},
{
"name": "index_message_received",
"unique": false,
"columnNames": [
"received"
],
"createSql": "CREATE INDEX `index_message_received` ON `${TABLE_NAME}` (`received`)"
},
{
"name": "index_message_ui_seen",
"unique": false,
"columnNames": [
"ui_seen"
],
"createSql": "CREATE INDEX `index_message_ui_seen` ON `${TABLE_NAME}` (`ui_seen`)"
},
{
"name": "index_message_ui_hide",
"unique": false,
"columnNames": [
"ui_hide"
],
"createSql": "CREATE INDEX `index_message_ui_hide` ON `${TABLE_NAME}` (`ui_hide`)"
},
{
"name": "index_message_ui_found",
"unique": false,
"columnNames": [
"ui_found"
],
"createSql": "CREATE INDEX `index_message_ui_found` ON `${TABLE_NAME}` (`ui_found`)"
}
],
"foreignKeys": [
{
"table": "account",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"account"
],
"referencedColumns": [
"id"
]
},
{
"table": "folder",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"folder"
],
"referencedColumns": [
"id"
]
},
{
"table": "identity",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"identity"
],
"referencedColumns": [
"id"
]
},
{
"table": "message",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"replying"
],
"referencedColumns": [
"id"
]
}
]
},
{
"tableName": "attachment",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `message` INTEGER NOT NULL, `sequence` INTEGER NOT NULL, `name` TEXT, `type` TEXT NOT NULL, `size` INTEGER, `progress` INTEGER, `available` INTEGER NOT NULL, FOREIGN KEY(`message`) REFERENCES `message`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "message",
"columnName": "message",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "sequence",
"columnName": "sequence",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "type",
"columnName": "type",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "size",
"columnName": "size",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "progress",
"columnName": "progress",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "available",
"columnName": "available",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": true
},
"indices": [
{
"name": "index_attachment_message",
"unique": false,
"columnNames": [
"message"
],
"createSql": "CREATE INDEX `index_attachment_message` ON `${TABLE_NAME}` (`message`)"
},
{
"name": "index_attachment_message_sequence",
"unique": true,
"columnNames": [
"message",
"sequence"
],
"createSql": "CREATE UNIQUE INDEX `index_attachment_message_sequence` ON `${TABLE_NAME}` (`message`, `sequence`)"
}
],
"foreignKeys": [
{
"table": "message",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"message"
],
"referencedColumns": [
"id"
]
}
]
},
{
"tableName": "operation",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `folder` INTEGER NOT NULL, `message` INTEGER NOT NULL, `name` TEXT NOT NULL, `args` TEXT NOT NULL, `created` INTEGER NOT NULL, FOREIGN KEY(`folder`) REFERENCES `folder`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`message`) REFERENCES `message`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "folder",
"columnName": "folder",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "message",
"columnName": "message",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "args",
"columnName": "args",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "created",
"columnName": "created",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": true
},
"indices": [
{
"name": "index_operation_folder",
"unique": false,
"columnNames": [
"folder"
],
"createSql": "CREATE INDEX `index_operation_folder` ON `${TABLE_NAME}` (`folder`)"
},
{
"name": "index_operation_message",
"unique": false,
"columnNames": [
"message"
],
"createSql": "CREATE INDEX `index_operation_message` ON `${TABLE_NAME}` (`message`)"
}
],
"foreignKeys": [
{
"table": "folder",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"folder"
],
"referencedColumns": [
"id"
]
},
{
"table": "message",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"message"
],
"referencedColumns": [
"id"
]
}
]
},
{
"tableName": "answer",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `name` TEXT NOT NULL, `text` TEXT NOT NULL)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "text",
"columnName": "text",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": true
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "log",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `time` INTEGER NOT NULL, `data` TEXT NOT NULL)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "time",
"columnName": "time",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "data",
"columnName": "data",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": true
},
"indices": [
{
"name": "index_log_time",
"unique": false,
"columnNames": [
"time"
],
"createSql": "CREATE INDEX `index_log_time` ON `${TABLE_NAME}` (`time`)"
}
],
"foreignKeys": []
}
],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, \"6127ad940456ed43d7551f7cd7b7ed18\")"
]
}
}

View File

@@ -0,0 +1,905 @@
{
"formatVersion": 1,
"database": {
"version": 9,
"identityHash": "67fade7db3a87ec2ef27dcec483c456f",
"entities": [
{
"tableName": "identity",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `name` TEXT NOT NULL, `email` TEXT NOT NULL, `replyto` TEXT, `account` INTEGER NOT NULL, `host` TEXT NOT NULL, `port` INTEGER NOT NULL, `starttls` INTEGER NOT NULL, `user` TEXT NOT NULL, `password` TEXT NOT NULL, `auth_type` INTEGER NOT NULL, `primary` INTEGER NOT NULL, `synchronize` INTEGER NOT NULL, `store_sent` INTEGER NOT NULL, `state` TEXT, `error` TEXT, FOREIGN KEY(`account`) REFERENCES `account`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "email",
"columnName": "email",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "replyto",
"columnName": "replyto",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "account",
"columnName": "account",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "host",
"columnName": "host",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "port",
"columnName": "port",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "starttls",
"columnName": "starttls",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "user",
"columnName": "user",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "password",
"columnName": "password",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "auth_type",
"columnName": "auth_type",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "primary",
"columnName": "primary",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "synchronize",
"columnName": "synchronize",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "store_sent",
"columnName": "store_sent",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "state",
"columnName": "state",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "error",
"columnName": "error",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": true
},
"indices": [
{
"name": "index_identity_account",
"unique": false,
"columnNames": [
"account"
],
"createSql": "CREATE INDEX `index_identity_account` ON `${TABLE_NAME}` (`account`)"
}
],
"foreignKeys": [
{
"table": "account",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"account"
],
"referencedColumns": [
"id"
]
}
]
},
{
"tableName": "account",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `name` TEXT, `host` TEXT NOT NULL, `port` INTEGER NOT NULL, `user` TEXT NOT NULL, `password` TEXT NOT NULL, `auth_type` INTEGER NOT NULL, `primary` INTEGER NOT NULL, `synchronize` INTEGER NOT NULL, `store_sent` INTEGER NOT NULL, `poll_interval` INTEGER NOT NULL, `seen_until` INTEGER, `state` TEXT, `error` TEXT)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "host",
"columnName": "host",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "port",
"columnName": "port",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "user",
"columnName": "user",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "password",
"columnName": "password",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "auth_type",
"columnName": "auth_type",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "primary",
"columnName": "primary",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "synchronize",
"columnName": "synchronize",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "store_sent",
"columnName": "store_sent",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "poll_interval",
"columnName": "poll_interval",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "seen_until",
"columnName": "seen_until",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "state",
"columnName": "state",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "error",
"columnName": "error",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": true
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "folder",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `account` INTEGER, `name` TEXT NOT NULL, `type` TEXT NOT NULL, `synchronize` INTEGER NOT NULL, `after` INTEGER NOT NULL, `state` TEXT, `error` TEXT, FOREIGN KEY(`account`) REFERENCES `account`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "account",
"columnName": "account",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "type",
"columnName": "type",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "synchronize",
"columnName": "synchronize",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "after",
"columnName": "after",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "state",
"columnName": "state",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "error",
"columnName": "error",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": true
},
"indices": [
{
"name": "index_folder_account_name",
"unique": true,
"columnNames": [
"account",
"name"
],
"createSql": "CREATE UNIQUE INDEX `index_folder_account_name` ON `${TABLE_NAME}` (`account`, `name`)"
},
{
"name": "index_folder_account",
"unique": false,
"columnNames": [
"account"
],
"createSql": "CREATE INDEX `index_folder_account` ON `${TABLE_NAME}` (`account`)"
},
{
"name": "index_folder_name",
"unique": false,
"columnNames": [
"name"
],
"createSql": "CREATE INDEX `index_folder_name` ON `${TABLE_NAME}` (`name`)"
},
{
"name": "index_folder_type",
"unique": false,
"columnNames": [
"type"
],
"createSql": "CREATE INDEX `index_folder_type` ON `${TABLE_NAME}` (`type`)"
}
],
"foreignKeys": [
{
"table": "account",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"account"
],
"referencedColumns": [
"id"
]
}
]
},
{
"tableName": "message",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `account` INTEGER, `folder` INTEGER NOT NULL, `identity` INTEGER, `replying` INTEGER, `uid` INTEGER, `msgid` TEXT, `references` TEXT, `inreplyto` TEXT, `thread` TEXT, `from` TEXT, `to` TEXT, `cc` TEXT, `bcc` TEXT, `reply` TEXT, `headers` TEXT, `subject` TEXT, `sent` INTEGER, `received` INTEGER NOT NULL, `stored` INTEGER NOT NULL, `seen` INTEGER NOT NULL, `ui_seen` INTEGER NOT NULL, `ui_hide` INTEGER NOT NULL, `ui_found` INTEGER NOT NULL, `error` TEXT, FOREIGN KEY(`account`) REFERENCES `account`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`folder`) REFERENCES `folder`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`identity`) REFERENCES `identity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`replying`) REFERENCES `message`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "account",
"columnName": "account",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "folder",
"columnName": "folder",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "identity",
"columnName": "identity",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "replying",
"columnName": "replying",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "uid",
"columnName": "uid",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "msgid",
"columnName": "msgid",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "references",
"columnName": "references",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "inreplyto",
"columnName": "inreplyto",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "thread",
"columnName": "thread",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "from",
"columnName": "from",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "to",
"columnName": "to",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "cc",
"columnName": "cc",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "bcc",
"columnName": "bcc",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "reply",
"columnName": "reply",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "headers",
"columnName": "headers",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "subject",
"columnName": "subject",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "sent",
"columnName": "sent",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "received",
"columnName": "received",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "stored",
"columnName": "stored",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "seen",
"columnName": "seen",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "ui_seen",
"columnName": "ui_seen",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "ui_hide",
"columnName": "ui_hide",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "ui_found",
"columnName": "ui_found",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "error",
"columnName": "error",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": true
},
"indices": [
{
"name": "index_message_account",
"unique": false,
"columnNames": [
"account"
],
"createSql": "CREATE INDEX `index_message_account` ON `${TABLE_NAME}` (`account`)"
},
{
"name": "index_message_folder",
"unique": false,
"columnNames": [
"folder"
],
"createSql": "CREATE INDEX `index_message_folder` ON `${TABLE_NAME}` (`folder`)"
},
{
"name": "index_message_identity",
"unique": false,
"columnNames": [
"identity"
],
"createSql": "CREATE INDEX `index_message_identity` ON `${TABLE_NAME}` (`identity`)"
},
{
"name": "index_message_replying",
"unique": false,
"columnNames": [
"replying"
],
"createSql": "CREATE INDEX `index_message_replying` ON `${TABLE_NAME}` (`replying`)"
},
{
"name": "index_message_folder_uid",
"unique": true,
"columnNames": [
"folder",
"uid"
],
"createSql": "CREATE UNIQUE INDEX `index_message_folder_uid` ON `${TABLE_NAME}` (`folder`, `uid`)"
},
{
"name": "index_message_msgid_folder",
"unique": true,
"columnNames": [
"msgid",
"folder"
],
"createSql": "CREATE UNIQUE INDEX `index_message_msgid_folder` ON `${TABLE_NAME}` (`msgid`, `folder`)"
},
{
"name": "index_message_thread",
"unique": false,
"columnNames": [
"thread"
],
"createSql": "CREATE INDEX `index_message_thread` ON `${TABLE_NAME}` (`thread`)"
},
{
"name": "index_message_received",
"unique": false,
"columnNames": [
"received"
],
"createSql": "CREATE INDEX `index_message_received` ON `${TABLE_NAME}` (`received`)"
},
{
"name": "index_message_ui_seen",
"unique": false,
"columnNames": [
"ui_seen"
],
"createSql": "CREATE INDEX `index_message_ui_seen` ON `${TABLE_NAME}` (`ui_seen`)"
},
{
"name": "index_message_ui_hide",
"unique": false,
"columnNames": [
"ui_hide"
],
"createSql": "CREATE INDEX `index_message_ui_hide` ON `${TABLE_NAME}` (`ui_hide`)"
},
{
"name": "index_message_ui_found",
"unique": false,
"columnNames": [
"ui_found"
],
"createSql": "CREATE INDEX `index_message_ui_found` ON `${TABLE_NAME}` (`ui_found`)"
}
],
"foreignKeys": [
{
"table": "account",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"account"
],
"referencedColumns": [
"id"
]
},
{
"table": "folder",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"folder"
],
"referencedColumns": [
"id"
]
},
{
"table": "identity",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"identity"
],
"referencedColumns": [
"id"
]
},
{
"table": "message",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"replying"
],
"referencedColumns": [
"id"
]
}
]
},
{
"tableName": "attachment",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `message` INTEGER NOT NULL, `sequence` INTEGER NOT NULL, `name` TEXT, `type` TEXT NOT NULL, `size` INTEGER, `progress` INTEGER, `available` INTEGER NOT NULL, FOREIGN KEY(`message`) REFERENCES `message`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "message",
"columnName": "message",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "sequence",
"columnName": "sequence",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "type",
"columnName": "type",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "size",
"columnName": "size",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "progress",
"columnName": "progress",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "available",
"columnName": "available",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": true
},
"indices": [
{
"name": "index_attachment_message",
"unique": false,
"columnNames": [
"message"
],
"createSql": "CREATE INDEX `index_attachment_message` ON `${TABLE_NAME}` (`message`)"
},
{
"name": "index_attachment_message_sequence",
"unique": true,
"columnNames": [
"message",
"sequence"
],
"createSql": "CREATE UNIQUE INDEX `index_attachment_message_sequence` ON `${TABLE_NAME}` (`message`, `sequence`)"
}
],
"foreignKeys": [
{
"table": "message",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"message"
],
"referencedColumns": [
"id"
]
}
]
},
{
"tableName": "operation",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `folder` INTEGER NOT NULL, `message` INTEGER NOT NULL, `name` TEXT NOT NULL, `args` TEXT NOT NULL, `created` INTEGER NOT NULL, FOREIGN KEY(`folder`) REFERENCES `folder`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`message`) REFERENCES `message`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "folder",
"columnName": "folder",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "message",
"columnName": "message",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "args",
"columnName": "args",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "created",
"columnName": "created",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": true
},
"indices": [
{
"name": "index_operation_folder",
"unique": false,
"columnNames": [
"folder"
],
"createSql": "CREATE INDEX `index_operation_folder` ON `${TABLE_NAME}` (`folder`)"
},
{
"name": "index_operation_message",
"unique": false,
"columnNames": [
"message"
],
"createSql": "CREATE INDEX `index_operation_message` ON `${TABLE_NAME}` (`message`)"
}
],
"foreignKeys": [
{
"table": "folder",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"folder"
],
"referencedColumns": [
"id"
]
},
{
"table": "message",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"message"
],
"referencedColumns": [
"id"
]
}
]
},
{
"tableName": "answer",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `name` TEXT NOT NULL, `text` TEXT NOT NULL)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "text",
"columnName": "text",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": true
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "log",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `time` INTEGER NOT NULL, `data` TEXT NOT NULL)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "time",
"columnName": "time",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "data",
"columnName": "data",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": true
},
"indices": [
{
"name": "index_log_time",
"unique": false,
"columnNames": [
"time"
],
"createSql": "CREATE INDEX `index_log_time` ON `${TABLE_NAME}` (`time`)"
}
],
"foreignKeys": []
}
],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, \"67fade7db3a87ec2ef27dcec483c456f\")"
]
}
}

View File

@@ -62,17 +62,32 @@
android:parentActivityName=".ActivityView">
<intent-filter>
<action android:name="android.intent.action.SEND" />
<action android:name="android.intent.action.SENDTO" />
<action android:name="android.intent.action.SEND_MULTIPLE" />
<data android:mimeType="*/*" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.SENDTO" />
<data android:scheme="mailto" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<data android:scheme="mailto" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
</intent-filter>
</activity>
<service android:name=".ServiceSynchronize" />
<service
android:name=".JobDaily"
android:exported="true"
android:permission="android.permission.BIND_JOB_SERVICE" />
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 12 KiB

View File

@@ -0,0 +1,214 @@
package eu.faircode.email;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.preference.PreferenceManager;
import android.provider.Settings;
import android.util.Log;
import android.view.View;
import android.widget.Toast;
import com.android.billingclient.api.BillingClient;
import com.android.billingclient.api.BillingClientStateListener;
import com.android.billingclient.api.BillingFlowParams;
import com.android.billingclient.api.Purchase;
import com.android.billingclient.api.PurchasesUpdatedListener;
import com.google.android.material.snackbar.Snackbar;
import java.security.NoSuchAlgorithmException;
import java.util.List;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
abstract class ActivityBilling extends ActivityBase implements PurchasesUpdatedListener {
private BillingClient billingClient = null;
static final String ACTION_PURCHASE = BuildConfig.APPLICATION_ID + ".ACTION_PURCHASE";
static final String ACTION_ACTIVATE_PRO = BuildConfig.APPLICATION_ID + ".ACTIVATE_PRO";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (Helper.isPlayStoreInstall(this)) {
billingClient = BillingClient.newBuilder(this).setListener(this).build();
billingClient.startConnection(billingClientStateListener);
}
}
@Override
protected void onResume() {
super.onResume();
LocalBroadcastManager lbm = LocalBroadcastManager.getInstance(this);
IntentFilter iff = new IntentFilter();
iff.addAction(ACTION_PURCHASE);
iff.addAction(ACTION_ACTIVATE_PRO);
lbm.registerReceiver(receiver, iff);
if (billingClient != null && billingClient.isReady())
queryPurchases();
}
@Override
protected void onPause() {
super.onPause();
LocalBroadcastManager lbm = LocalBroadcastManager.getInstance(this);
lbm.unregisterReceiver(receiver);
}
@Override
protected void onDestroy() {
if (billingClient != null)
billingClient.endConnection();
super.onDestroy();
}
protected Intent getIntentPro() {
if (Helper.isPlayStoreInstall(this))
return null;
try {
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setData(Uri.parse("https://email.faircode.eu/pro/?challenge=" + getChallenge()));
return intent;
} catch (NoSuchAlgorithmException ex) {
Log.e(Helper.TAG, ex + "\n" + Log.getStackTraceString(ex));
return null;
}
}
private String getChallenge() throws NoSuchAlgorithmException {
String android_id = Settings.Secure.getString(getContentResolver(), Settings.Secure.ANDROID_ID);
return Helper.sha256(android_id);
}
private String getResponse() throws NoSuchAlgorithmException {
return Helper.sha256(BuildConfig.APPLICATION_ID + getChallenge());
}
BroadcastReceiver receiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
if (ACTION_PURCHASE.equals(intent.getAction()))
onPurchase(intent);
else if (ACTION_ACTIVATE_PRO.equals(intent.getAction()))
onActivatePro(intent);
}
};
private View getView() {
return findViewById(android.R.id.content);
}
private void onPurchase(Intent intent) {
if (Helper.isPlayStoreInstall(this)) {
BillingFlowParams flowParams = BillingFlowParams.newBuilder()
.setSku(BuildConfig.APPLICATION_ID + ".pro")
.setType(BillingClient.SkuType.INAPP)
.build();
int responseCode = billingClient.launchBillingFlow(this, flowParams);
String text = Helper.getBillingResponseText(responseCode);
Log.i(Helper.TAG, "IAB launch billing flow response=" + text);
if (responseCode != BillingClient.BillingResponse.OK)
Snackbar.make(getView(), text, Snackbar.LENGTH_LONG).show();
} else
startActivity(getIntentPro());
}
private void onActivatePro(Intent intent) {
try {
Uri data = intent.getParcelableExtra("uri");
String challenge = getChallenge();
String response = data.getQueryParameter("response");
Log.i(Helper.TAG, "Challenge=" + challenge);
Log.i(Helper.TAG, "Response=" + response);
String expected = getResponse();
if (expected.equals(response)) {
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
prefs.edit().putBoolean("pro", true).apply();
Log.i(Helper.TAG, "Response valid");
Snackbar.make(getView(), R.string.title_pro_valid, Snackbar.LENGTH_LONG).show();
} else {
Log.i(Helper.TAG, "Response invalid");
Snackbar.make(getView(), R.string.title_pro_invalid, Snackbar.LENGTH_LONG).show();
}
intent.setData(null);
setIntent(intent);
} catch (NoSuchAlgorithmException ex) {
Log.e(Helper.TAG, Log.getStackTraceString(ex));
Toast.makeText(this, ex.getMessage(), Toast.LENGTH_LONG).show();
}
}
private BillingClientStateListener billingClientStateListener = new BillingClientStateListener() {
private int backoff = 4; // seconds
@Override
public void onBillingSetupFinished(@BillingClient.BillingResponse int responseCode) {
String text = Helper.getBillingResponseText(responseCode);
Log.i(Helper.TAG, "IAB connected response=" + text);
if (responseCode == BillingClient.BillingResponse.OK) {
backoff = 4;
queryPurchases();
} else
Snackbar.make(getView(), text, Snackbar.LENGTH_LONG).show();
}
@Override
public void onBillingServiceDisconnected() {
backoff *= 2;
Log.i(Helper.TAG, "IAB disconnected retry in " + backoff + " s");
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
if (!billingClient.isReady())
billingClient.startConnection(billingClientStateListener);
}
}, backoff * 1000L);
}
};
@Override
public void onPurchasesUpdated(int responseCode, @android.support.annotation.Nullable List<Purchase> purchases) {
String text = Helper.getBillingResponseText(responseCode);
Log.i(Helper.TAG, "IAB purchases updated response=" + text);
if (responseCode == BillingClient.BillingResponse.OK)
checkPurchases(purchases);
else
Snackbar.make(getView(), text, Snackbar.LENGTH_LONG).show();
}
private void queryPurchases() {
Purchase.PurchasesResult result = billingClient.queryPurchases(BillingClient.SkuType.INAPP);
String text = Helper.getBillingResponseText(result.getResponseCode());
Log.i(Helper.TAG, "IAB query purchases response=" + text);
if (result.getResponseCode() == BillingClient.BillingResponse.OK)
checkPurchases(result.getPurchasesList());
else
Snackbar.make(getView(), text, Snackbar.LENGTH_LONG).show();
}
private void checkPurchases(List<Purchase> purchases) {
if (purchases != null) {
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
SharedPreferences.Editor editor = prefs.edit();
editor.remove("pro");
for (Purchase purchase : purchases) {
Log.i(Helper.TAG, "IAB SKU=" + purchase.getSku());
if ((BuildConfig.APPLICATION_ID + ".pro").equals(purchase.getSku())) {
editor.putBoolean("pro", true);
Log.i(Helper.TAG, "IAB pro features activated");
}
}
editor.apply();
}
}
}

View File

@@ -31,12 +31,11 @@ import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentTransaction;
import androidx.lifecycle.Lifecycle;
public class ActivityCompose extends ActivityBase implements FragmentManager.OnBackStackChangedListener {
public class ActivityCompose extends ActivityBilling implements FragmentManager.OnBackStackChangedListener {
static final int REQUEST_CONTACT_TO = 1;
static final int REQUEST_CONTACT_CC = 2;
static final int REQUEST_CONTACT_BCC = 3;
static final int REQUEST_ATTACHMENT = 4;
static final int REQUEST_OPENPGP = 5;
@Override
protected void onCreate(Bundle savedInstanceState) {
@@ -49,40 +48,45 @@ public class ActivityCompose extends ActivityBase implements FragmentManager.OnB
if (getSupportFragmentManager().getFragments().size() == 0) {
Bundle args;
if (Intent.ACTION_SEND.equals(getIntent().getAction()) ||
Intent.ACTION_SENDTO.equals(getIntent().getAction()) ||
Intent.ACTION_SEND_MULTIPLE.equals(getIntent().getAction())) {
Intent intent = getIntent();
String action = intent.getAction();
if (Intent.ACTION_VIEW.equals(action) ||
Intent.ACTION_SENDTO.equals(action) ||
Intent.ACTION_SEND.equals(action) ||
Intent.ACTION_SEND_MULTIPLE.equals(action)) {
args = new Bundle();
args.putString("action", "new");
args.putLong("account", -1);
if (getIntent().hasExtra(Intent.EXTRA_EMAIL))
args.putString("to", TextUtils.join(", ", getIntent().getStringArrayExtra(Intent.EXTRA_EMAIL)));
Uri uri = intent.getData();
if (uri != null && "mailto".equals(uri.getScheme()))
args.putString("to", uri.getSchemeSpecificPart());
if (getIntent().hasExtra(Intent.EXTRA_CC))
args.putString("cc", TextUtils.join(", ", getIntent().getStringArrayExtra(Intent.EXTRA_CC)));
if (intent.hasExtra(Intent.EXTRA_EMAIL))
args.putString("to", TextUtils.join(", ", intent.getStringArrayExtra(Intent.EXTRA_EMAIL)));
if (getIntent().hasExtra(Intent.EXTRA_BCC))
args.putString("bcc", TextUtils.join(", ", getIntent().getStringArrayExtra(Intent.EXTRA_BCC)));
if (intent.hasExtra(Intent.EXTRA_CC))
args.putString("cc", TextUtils.join(", ", intent.getStringArrayExtra(Intent.EXTRA_CC)));
if (getIntent().hasExtra(Intent.EXTRA_SUBJECT))
args.putString("subject", getIntent().getStringExtra(Intent.EXTRA_SUBJECT));
if (intent.hasExtra(Intent.EXTRA_BCC))
args.putString("bcc", TextUtils.join(", ", intent.getStringArrayExtra(Intent.EXTRA_BCC)));
if (getIntent().hasExtra(Intent.EXTRA_TEXT))
args.putString("body", getIntent().getStringExtra(Intent.EXTRA_TEXT)); // Intent.EXTRA_HTML_TEXT
if (intent.hasExtra(Intent.EXTRA_SUBJECT))
args.putString("subject", intent.getStringExtra(Intent.EXTRA_SUBJECT));
if (getIntent().hasExtra(Intent.EXTRA_STREAM))
if (Intent.ACTION_SEND_MULTIPLE.equals(getIntent().getAction()))
args.putParcelableArrayList("attachments", getIntent().getParcelableArrayListExtra(Intent.EXTRA_STREAM));
if (intent.hasExtra(Intent.EXTRA_TEXT))
args.putString("body", intent.getStringExtra(Intent.EXTRA_TEXT)); // Intent.EXTRA_HTML_TEXT
if (intent.hasExtra(Intent.EXTRA_STREAM))
if (Intent.ACTION_SEND_MULTIPLE.equals(action))
args.putParcelableArrayList("attachments", intent.getParcelableArrayListExtra(Intent.EXTRA_STREAM));
else {
ArrayList<Uri> uris = new ArrayList<>();
uris.add((Uri) getIntent().getParcelableExtra(Intent.EXTRA_STREAM));
uris.add((Uri) intent.getParcelableExtra(Intent.EXTRA_STREAM));
args.putParcelableArrayList("attachments", uris);
}
} else
args = getIntent().getExtras();
args = intent.getExtras();
FragmentCompose fragment = new FragmentCompose();
fragment.setArguments(args);

View File

@@ -34,8 +34,8 @@ import androidx.lifecycle.Lifecycle;
import androidx.lifecycle.Observer;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
public class ActivitySetup extends ActivityBase implements FragmentManager.OnBackStackChangedListener {
boolean hasAccount;
public class ActivitySetup extends ActivityBilling implements FragmentManager.OnBackStackChangedListener {
private boolean hasAccount;
static final int REQUEST_PERMISSION = 1;
static final int REQUEST_CHOOSE_ACCOUNT = 2;

View File

@@ -22,17 +22,15 @@ package eu.faircode.email;
import android.app.Activity;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.content.res.Configuration;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.ParcelFileDescriptor;
import android.preference.PreferenceManager;
import android.provider.Settings;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.MenuItem;
@@ -45,19 +43,11 @@ import android.widget.ListView;
import android.widget.TextView;
import android.widget.Toast;
import com.android.billingclient.api.BillingClient;
import com.android.billingclient.api.BillingClientStateListener;
import com.android.billingclient.api.BillingFlowParams;
import com.android.billingclient.api.Purchase;
import com.android.billingclient.api.PurchasesUpdatedListener;
import com.google.android.material.snackbar.Snackbar;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.security.NoSuchAlgorithmException;
import java.text.Collator;
import java.util.ArrayList;
import java.util.Collections;
@@ -71,6 +61,7 @@ import javax.mail.Address;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.ActionBarDrawerToggle;
import androidx.appcompat.app.AlertDialog;
import androidx.drawerlayout.widget.DrawerLayout;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentTransaction;
@@ -78,12 +69,11 @@ import androidx.lifecycle.Lifecycle;
import androidx.lifecycle.Observer;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
public class ActivityView extends ActivityBase implements FragmentManager.OnBackStackChangedListener, PurchasesUpdatedListener {
public class ActivityView extends ActivityBilling implements FragmentManager.OnBackStackChangedListener {
private View view;
private DrawerLayout drawerLayout;
private ListView drawerList;
private ActionBarDrawerToggle drawerToggle;
private BillingClient billingClient = null;
private boolean newIntent = false;
private long attachment = -1;
@@ -94,15 +84,12 @@ public class ActivityView extends ActivityBase implements FragmentManager.OnBack
static final int REQUEST_UNSEEN = 2;
static final int REQUEST_ATTACHMENT = 1;
static final int REQUEST_OPENPGP = 2;
static final String ACTION_VIEW_MESSAGES = BuildConfig.APPLICATION_ID + ".VIEW_MESSAGES";
static final String ACTION_VIEW_MESSAGE = BuildConfig.APPLICATION_ID + ".VIEW_MESSAGE";
static final String ACTION_EDIT_FOLDER = BuildConfig.APPLICATION_ID + ".EDIT_FOLDER";
static final String ACTION_EDIT_ANSWER = BuildConfig.APPLICATION_ID + ".EDIT_ANSWER";
static final String ACTION_STORE_ATTACHMENT = BuildConfig.APPLICATION_ID + ".STORE_ATTACHMENT";
static final String ACTION_PURCHASE = BuildConfig.APPLICATION_ID + ".ACTION_PURCHASE";
static final String ACTION_ACTIVATE_PRO = BuildConfig.APPLICATION_ID + ".ACTIVATE_PRO";
@Override
protected void onCreate(Bundle savedInstanceState) {
@@ -163,6 +150,9 @@ public class ActivityView extends ActivityBase implements FragmentManager.OnBack
case R.string.menu_about:
onMenuAbout();
break;
case R.string.menu_rate:
onMenuRate();
break;
case R.string.menu_other:
onMenuOtherApps();
break;
@@ -200,6 +190,8 @@ public class ActivityView extends ActivityBase implements FragmentManager.OnBack
drawerArray.add(new DrawerItem(ActivityView.this, R.layout.item_drawer, R.drawable.baseline_settings_applications_24, R.string.menu_setup));
drawerArray.add(new DrawerItem(ActivityView.this, R.layout.item_drawer, R.drawable.baseline_reply_24, R.string.menu_answers));
drawerArray.add(new DrawerItem(R.layout.item_drawer_separator));
if (PreferenceManager.getDefaultSharedPreferences(ActivityView.this).getBoolean("debug", false))
drawerArray.add(new DrawerItem(ActivityView.this, R.layout.item_drawer, R.drawable.baseline_list_24, R.string.menu_operations));
@@ -217,6 +209,11 @@ public class ActivityView extends ActivityBase implements FragmentManager.OnBack
drawerArray.add(new DrawerItem(ActivityView.this, R.layout.item_drawer, R.drawable.baseline_info_24, R.string.menu_about));
drawerArray.add(new DrawerItem(R.layout.item_drawer_separator));
if (getIntentRate().resolveActivity(getPackageManager()) != null)
drawerArray.add(new DrawerItem(ActivityView.this, R.layout.item_drawer, R.drawable.baseline_star_24, R.string.menu_rate));
if (getIntentOtherApps().resolveActivity(getPackageManager()) != null)
drawerArray.add(new DrawerItem(ActivityView.this, R.layout.item_drawer, R.drawable.baseline_get_app_24, R.string.menu_other));
@@ -293,7 +290,10 @@ public class ActivityView extends ActivityBase implements FragmentManager.OnBack
draft.received = new Date().getTime();
draft.seen = false;
draft.ui_seen = false;
draft.flagged = false;
draft.ui_flagged = false;
draft.ui_hide = false;
draft.ui_found = false;
draft.id = db.message().insertMessage(draft);
draft.write(context, body);
}
@@ -324,11 +324,6 @@ public class ActivityView extends ActivityBase implements FragmentManager.OnBack
}
}.load(this, new Bundle());
if (Helper.isPlayStoreInstall(this)) {
billingClient = BillingClient.newBuilder(this).setListener(this).build();
billingClient.startConnection(billingClientStateListener);
}
checkIntent(getIntent());
}
@@ -365,17 +360,12 @@ public class ActivityView extends ActivityBase implements FragmentManager.OnBack
iff.addAction(ACTION_EDIT_FOLDER);
iff.addAction(ACTION_EDIT_ANSWER);
iff.addAction(ACTION_STORE_ATTACHMENT);
iff.addAction(ACTION_PURCHASE);
iff.addAction(ACTION_ACTIVATE_PRO);
lbm.registerReceiver(receiver, iff);
if (newIntent) {
newIntent = false;
getSupportFragmentManager().popBackStack("unified", 0);
}
if (billingClient != null && billingClient.isReady())
queryPurchases();
}
@Override
@@ -391,13 +381,6 @@ public class ActivityView extends ActivityBase implements FragmentManager.OnBack
drawerToggle.onConfigurationChanged(newConfig);
}
@Override
protected void onDestroy() {
if (billingClient != null)
billingClient.endConnection();
super.onDestroy();
}
@Override
public void onBackPressed() {
if (drawerLayout.isDrawerOpen(drawerList))
@@ -433,15 +416,6 @@ public class ActivityView extends ActivityBase implements FragmentManager.OnBack
}
}
private String getChallenge() throws NoSuchAlgorithmException {
String android_id = Settings.Secure.getString(getContentResolver(), Settings.Secure.ANDROID_ID);
return Helper.sha256(android_id);
}
private String getResponse() throws NoSuchAlgorithmException {
return Helper.sha256(BuildConfig.APPLICATION_ID + getChallenge());
}
private void checkIntent(Intent intent) {
Log.i(Helper.TAG, "View intent=" + intent + " action=" + intent.getAction());
String action = intent.getAction();
@@ -492,18 +466,11 @@ public class ActivityView extends ActivityBase implements FragmentManager.OnBack
return intent;
}
private Intent getIntentPro() {
if (Helper.isPlayStoreInstall(this))
return null;
try {
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setData(Uri.parse("https://email.faircode.eu/pro/?challenge=" + getChallenge()));
return intent;
} catch (NoSuchAlgorithmException ex) {
Log.e(Helper.TAG, ex + "\n" + Log.getStackTraceString(ex));
return null;
}
private Intent getIntentRate() {
Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse("market://details?id=" + BuildConfig.APPLICATION_ID));
if (intent.resolveActivity(getPackageManager()) == null)
intent = new Intent(Intent.ACTION_VIEW, Uri.parse("https://play.google.com/store/apps/details?id=" + BuildConfig.APPLICATION_ID));
return intent;
}
private Intent getIntentOtherApps() {
@@ -568,6 +535,30 @@ public class ActivityView extends ActivityBase implements FragmentManager.OnBack
fragmentTransaction.commit();
}
private void onMenuRate() {
Intent faq = getIntentFAQ();
if (faq.resolveActivity(getPackageManager()) == null)
startActivity(getIntentRate());
else {
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder
.setMessage(R.string.title_issue)
.setPositiveButton(R.string.title_yes, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
startActivity(getIntentFAQ());
}
})
.setNegativeButton(R.string.title_no, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
startActivity(getIntentRate());
}
})
.show();
}
}
private void onMenuOtherApps() {
startActivity(getIntentOtherApps());
}
@@ -642,10 +633,6 @@ public class ActivityView extends ActivityBase implements FragmentManager.OnBack
onEditAnswer(intent);
else if (ACTION_STORE_ATTACHMENT.equals(intent.getAction()))
onStoreAttachment(intent);
else if (ACTION_PURCHASE.equals(intent.getAction()))
onPurchase(intent);
else if (ACTION_ACTIVATE_PRO.equals(intent.getAction()))
onActivatePro(intent);
}
};
@@ -723,47 +710,6 @@ public class ActivityView extends ActivityBase implements FragmentManager.OnBack
startActivityForResult(create, REQUEST_ATTACHMENT);
}
private void onPurchase(Intent intent) {
if (Helper.isPlayStoreInstall(this)) {
BillingFlowParams flowParams = BillingFlowParams.newBuilder()
.setSku(BuildConfig.APPLICATION_ID + ".pro")
.setType(BillingClient.SkuType.INAPP)
.build();
int responseCode = billingClient.launchBillingFlow(ActivityView.this, flowParams);
String text = Helper.getBillingResponseText(responseCode);
Log.i(Helper.TAG, "IAB launch billing flow response=" + text);
if (responseCode != BillingClient.BillingResponse.OK)
Snackbar.make(view, text, Snackbar.LENGTH_LONG).show();
} else
startActivity(getIntentPro());
}
private void onActivatePro(Intent intent) {
try {
Uri data = intent.getParcelableExtra("uri");
String challenge = getChallenge();
String response = data.getQueryParameter("response");
Log.i(Helper.TAG, "Challenge=" + challenge);
Log.i(Helper.TAG, "Response=" + response);
String expected = getResponse();
if (expected.equals(response)) {
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(ActivityView.this);
prefs.edit().putBoolean("pro", true).apply();
Log.i(Helper.TAG, "Response valid");
Snackbar.make(view, R.string.title_pro_valid, Snackbar.LENGTH_LONG).show();
} else {
Log.i(Helper.TAG, "Response invalid");
Snackbar.make(view, R.string.title_pro_invalid, Snackbar.LENGTH_LONG).show();
}
intent.setData(null);
setIntent(intent);
} catch (NoSuchAlgorithmException ex) {
Log.e(Helper.TAG, Log.getStackTraceString(ex));
Toast.makeText(ActivityView.this, ex.getMessage(), Toast.LENGTH_LONG).show();
}
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
Log.i(Helper.TAG, "View onActivityResult request=" + requestCode + " result=" + resultCode + " data=" + data);
@@ -830,68 +776,4 @@ public class ActivityView extends ActivityBase implements FragmentManager.OnBack
}.load(this, args);
}
}
private BillingClientStateListener billingClientStateListener = new BillingClientStateListener() {
private int backoff = 4; // seconds
@Override
public void onBillingSetupFinished(@BillingClient.BillingResponse int responseCode) {
String text = Helper.getBillingResponseText(responseCode);
Log.i(Helper.TAG, "IAB connected response=" + text);
if (responseCode == BillingClient.BillingResponse.OK) {
backoff = 4;
queryPurchases();
} else
Snackbar.make(view, text, Snackbar.LENGTH_LONG).show();
}
@Override
public void onBillingServiceDisconnected() {
backoff *= 2;
Log.i(Helper.TAG, "IAB disconnected retry in " + backoff + " s");
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
if (!billingClient.isReady())
billingClient.startConnection(billingClientStateListener);
}
}, backoff * 1000L);
}
};
@Override
public void onPurchasesUpdated(int responseCode, @android.support.annotation.Nullable List<Purchase> purchases) {
String text = Helper.getBillingResponseText(responseCode);
Log.i(Helper.TAG, "IAB purchases updated response=" + text);
if (responseCode == BillingClient.BillingResponse.OK)
checkPurchases(purchases);
else
Snackbar.make(view, text, Snackbar.LENGTH_LONG).show();
}
private void queryPurchases() {
Purchase.PurchasesResult result = billingClient.queryPurchases(BillingClient.SkuType.INAPP);
String text = Helper.getBillingResponseText(result.getResponseCode());
Log.i(Helper.TAG, "IAB query purchases response=" + text);
if (result.getResponseCode() == BillingClient.BillingResponse.OK)
checkPurchases(result.getPurchasesList());
else
Snackbar.make(view, text, Snackbar.LENGTH_LONG).show();
}
private void checkPurchases(List<Purchase> purchases) {
if (purchases != null) {
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
SharedPreferences.Editor editor = prefs.edit();
editor.remove("pro");
for (Purchase purchase : purchases) {
Log.i(Helper.TAG, "IAB SKU=" + purchase.getSku());
if ((BuildConfig.APPLICATION_ID + ".pro").equals(purchase.getSku())) {
editor.putBoolean("pro", true);
Log.i(Helper.TAG, "IAB pro features activated");
}
}
editor.apply();
}
}
}

View File

@@ -54,6 +54,7 @@ public class AdapterFolder extends RecyclerView.Adapter<AdapterFolder.ViewHolder
TextView tvName;
TextView tvMessages;
TextView tvType;
ImageView ivUnified;
TextView tvAfter;
ImageView ivSync;
ImageView ivState;
@@ -67,6 +68,7 @@ public class AdapterFolder extends RecyclerView.Adapter<AdapterFolder.ViewHolder
tvName = itemView.findViewById(R.id.tvName);
tvMessages = itemView.findViewById(R.id.tvMessages);
tvType = itemView.findViewById(R.id.tvType);
ivUnified = itemView.findViewById(R.id.ivUnified);
tvAfter = itemView.findViewById(R.id.tvAfter);
ivSync = itemView.findViewById(R.id.ivSync);
tvError = itemView.findViewById(R.id.tvError);
@@ -106,6 +108,8 @@ public class AdapterFolder extends RecyclerView.Adapter<AdapterFolder.ViewHolder
context.getPackageName());
tvType.setText(resid > 0 ? context.getString(resid) : folder.type);
ivUnified.setVisibility(folder.unified ? View.VISIBLE : View.GONE);
tvAfter.setText(Integer.toString(folder.after));
ivSync.setVisibility(folder.synchronize ? View.VISIBLE : View.INVISIBLE);
@@ -119,7 +123,7 @@ public class AdapterFolder extends RecyclerView.Adapter<AdapterFolder.ViewHolder
ivState.setImageResource(R.drawable.baseline_compare_arrows_24);
else
ivState.setImageResource(R.drawable.baseline_cloud_off_24);
ivState.setVisibility(folder.synchronize || outbox ? View.VISIBLE : View.INVISIBLE);
ivState.setVisibility(folder.synchronize || folder.state != null ? View.VISIBLE : View.INVISIBLE);
tvError.setText(folder.error);
tvError.setVisibility(folder.error == null ? View.GONE : View.VISIBLE);

View File

@@ -0,0 +1,162 @@
package eu.faircode.email;
/*
This file is part of FairEmail.
FairEmail is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
NetGuard is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with NetGuard. If not, see <http://www.gnu.org/licenses/>.
Copyright 2018 by Marcel Bokhorst (M66B)
*/
import android.content.Context;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.List;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.DiffUtil;
import androidx.recyclerview.widget.ListUpdateCallback;
import androidx.recyclerview.widget.RecyclerView;
public class AdapterLog extends RecyclerView.Adapter<AdapterLog.ViewHolder> {
private Context context;
private List<EntityLog> all = new ArrayList<>();
private List<EntityLog> filtered = new ArrayList<>();
private static final DateFormat DF = SimpleDateFormat.getTimeInstance();
public class ViewHolder extends RecyclerView.ViewHolder {
View itemView;
TextView tvTime;
TextView tvData;
ViewHolder(View itemView) {
super(itemView);
this.itemView = itemView;
tvTime = itemView.findViewById(R.id.tvTime);
tvData = itemView.findViewById(R.id.tvData);
}
private void bindTo(EntityLog log) {
tvTime.setText(DF.format(log.time));
tvData.setText(log.data);
}
}
AdapterLog(Context context) {
this.context = context;
setHasStableIds(true);
}
public void set(@NonNull List<EntityLog> logs) {
Log.i(Helper.TAG, "Set logs=" + logs.size());
all.clear();
all.addAll(logs);
DiffUtil.DiffResult diff = DiffUtil.calculateDiff(new MessageDiffCallback(filtered, all));
filtered.clear();
filtered.addAll(all);
diff.dispatchUpdatesTo(new ListUpdateCallback() {
@Override
public void onInserted(int position, int count) {
Log.i(Helper.TAG, "Inserted @" + position + " #" + count);
}
@Override
public void onRemoved(int position, int count) {
Log.i(Helper.TAG, "Removed @" + position + " #" + count);
}
@Override
public void onMoved(int fromPosition, int toPosition) {
Log.i(Helper.TAG, "Moved " + fromPosition + ">" + toPosition);
}
@Override
public void onChanged(int position, int count, Object payload) {
Log.i(Helper.TAG, "Changed @" + position + " #" + count);
}
});
diff.dispatchUpdatesTo(this);
}
private class MessageDiffCallback extends DiffUtil.Callback {
private List<EntityLog> prev;
private List<EntityLog> next;
MessageDiffCallback(List<EntityLog> prev, List<EntityLog> next) {
this.prev = prev;
this.next = next;
}
@Override
public int getOldListSize() {
return prev.size();
}
@Override
public int getNewListSize() {
return next.size();
}
@Override
public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) {
EntityLog l1 = prev.get(oldItemPosition);
EntityLog l2 = next.get(newItemPosition);
return l1.id.equals(l2.id);
}
@Override
public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) {
EntityLog l1 = prev.get(oldItemPosition);
EntityLog l2 = next.get(newItemPosition);
return l1.equals(l2);
}
}
@Override
public long getItemId(int position) {
return filtered.get(position).id;
}
@Override
public int getItemCount() {
return filtered.size();
}
@Override
@NonNull
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
return new ViewHolder(LayoutInflater.from(context).inflate(R.layout.item_log, parent, false));
}
@Override
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
EntityLog log = filtered.get(position);
holder.bindTo(log);
}
}

View File

@@ -22,14 +22,18 @@ package eu.faircode.email;
import android.content.Context;
import android.content.Intent;
import android.graphics.Typeface;
import android.os.Bundle;
import android.preference.PreferenceManager;
import android.text.format.DateUtils;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.ProgressBar;
import android.widget.TextView;
import android.widget.Toast;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
@@ -37,6 +41,7 @@ import java.util.Date;
import java.util.List;
import androidx.annotation.NonNull;
import androidx.appcompat.widget.PopupMenu;
import androidx.lifecycle.LifecycleOwner;
import androidx.lifecycle.Observer;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
@@ -57,27 +62,32 @@ public class AdapterMessage extends PagedListAdapter<TupleMessageEx, AdapterMess
public class ViewHolder extends RecyclerView.ViewHolder
implements View.OnClickListener, View.OnLongClickListener {
View itemView;
ImageView ivFlagged;
TextView tvFrom;
TextView tvSize;
TextView tvTime;
ImageView ivAttachments;
TextView tvSubject;
TextView tvFolder;
TextView tvCount;
ImageView ivThread;
TextView tvError;
ProgressBar pbLoading;
private static final int action_seen = 1;
private static final int action_flag = 2;
ViewHolder(View itemView) {
super(itemView);
this.itemView = itemView;
ivFlagged = itemView.findViewById(R.id.ivFlagged);
tvFrom = itemView.findViewById(R.id.tvFrom);
tvSize = itemView.findViewById(R.id.tvSize);
tvTime = itemView.findViewById(R.id.tvTime);
ivAttachments = itemView.findViewById(R.id.ivAttachments);
tvSubject = itemView.findViewById(R.id.tvSubject);
tvFolder = itemView.findViewById(R.id.tvFolder);
tvCount = itemView.findViewById(R.id.tvCount);
ivThread = itemView.findViewById(R.id.ivThread);
tvError = itemView.findViewById(R.id.tvError);
pbLoading = itemView.findViewById(R.id.pbLoading);
}
@@ -93,13 +103,15 @@ public class AdapterMessage extends PagedListAdapter<TupleMessageEx, AdapterMess
}
private void clear() {
ivFlagged.setVisibility(View.GONE);
tvFrom.setText(null);
tvSize.setText(null);
tvTime.setText(null);
tvSubject.setText(null);
ivAttachments.setVisibility(View.GONE);
tvSubject.setText(null);
tvFolder.setText(null);
tvCount.setText(null);
ivThread.setVisibility(View.GONE);
tvError.setText(null);
pbLoading.setVisibility(View.VISIBLE);
}
@@ -107,6 +119,8 @@ public class AdapterMessage extends PagedListAdapter<TupleMessageEx, AdapterMess
private void bindTo(final TupleMessageEx message) {
pbLoading.setVisibility(View.GONE);
ivFlagged.setVisibility(message.ui_flagged ? View.VISIBLE : View.GONE);
if (EntityFolder.DRAFTS.equals(message.folderType) ||
EntityFolder.OUTBOX.equals(message.folderType) ||
EntityFolder.SENT.equals(message.folderType)) {
@@ -117,8 +131,6 @@ public class AdapterMessage extends PagedListAdapter<TupleMessageEx, AdapterMess
tvTime.setText(DateUtils.getRelativeTimeSpanString(context, message.received));
}
tvSize.setVisibility(View.GONE);
tvSubject.setText(message.subject);
ivAttachments.setVisibility(message.attachments > 0 ? View.VISIBLE : View.GONE);
@@ -129,11 +141,12 @@ public class AdapterMessage extends PagedListAdapter<TupleMessageEx, AdapterMess
else
tvFolder.setText(Helper.localizeFolderName(context, message.folderName));
if (viewType == ViewType.THREAD)
if (viewType == ViewType.THREAD) {
tvCount.setVisibility(View.GONE);
else {
ivThread.setVisibility(View.GONE);
} else {
tvCount.setText(Integer.toString(message.count));
tvCount.setVisibility(debug || message.count > 1 ? View.VISIBLE : View.GONE);
ivThread.setVisibility(View.VISIBLE);
}
if (debug) {
@@ -200,12 +213,59 @@ public class AdapterMessage extends PagedListAdapter<TupleMessageEx, AdapterMess
if (pos == RecyclerView.NO_POSITION)
return false;
TupleMessageEx message = getItem(pos);
final TupleMessageEx message = getItem(pos);
LocalBroadcastManager lbm = LocalBroadcastManager.getInstance(context);
lbm.sendBroadcast(
new Intent(ActivityView.ACTION_VIEW_MESSAGE)
.putExtra("message", message));
PopupMenu popupMenu = new PopupMenu(context, itemView);
popupMenu.getMenu().add(Menu.NONE, action_flag, 1, message.ui_flagged ? R.string.title_unflag : R.string.title_flag);
popupMenu.getMenu().add(Menu.NONE, action_seen, 2, message.ui_seen ? R.string.title_unseen : R.string.title_seen);
popupMenu.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() {
@Override
public boolean onMenuItemClick(MenuItem target) {
Bundle args = new Bundle();
args.putLong("id", message.id);
args.putInt("action", target.getItemId());
new SimpleTask<Void>() {
@Override
protected Void onLoad(Context context, Bundle args) {
long id = args.getLong("id");
int action = args.getInt("action");
DB db = DB.getInstance(context);
try {
db.beginTransaction();
EntityMessage message = db.message().getMessage(id);
for (EntityMessage tmessage : db.message().getMessageByThread(message.account, message.thread))
if (action == action_flag) {
db.message().setMessageUiFlagged(tmessage.id, !message.ui_flagged);
EntityOperation.queue(db, tmessage, EntityOperation.FLAG, !tmessage.ui_flagged);
} else if (action == action_seen) {
db.message().setMessageUiSeen(tmessage.id, !message.ui_seen);
EntityOperation.queue(db, tmessage, EntityOperation.SEEN, !tmessage.ui_seen);
}
db.setTransactionSuccessful();
} finally {
db.endTransaction();
}
EntityOperation.process(context);
return null;
}
@Override
public void onException(Bundle args, Throwable ex) {
Toast.makeText(context, ex.toString(), Toast.LENGTH_LONG).show();
}
}.load(context, owner, args);
return true;
}
});
popupMenu.show();
return true;
}

View File

@@ -0,0 +1,197 @@
package eu.faircode.email;
/*
This file is part of FairEmail.
FairEmail is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
NetGuard is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with NetGuard. If not, see <http://www.gnu.org/licenses/>.
Copyright 2018 by Marcel Bokhorst (M66B)
*/
import android.content.Context;
import android.os.Handler;
import android.util.Log;
import com.sun.mail.imap.IMAPFolder;
import com.sun.mail.imap.IMAPMessage;
import com.sun.mail.imap.IMAPStore;
import java.util.Date;
import java.util.Properties;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import javax.mail.Folder;
import javax.mail.Message;
import javax.mail.Session;
import javax.mail.search.AndTerm;
import javax.mail.search.BodyTerm;
import javax.mail.search.ComparisonTerm;
import javax.mail.search.FromStringTerm;
import javax.mail.search.OrTerm;
import javax.mail.search.ReceivedDateTerm;
import javax.mail.search.SubjectTerm;
import androidx.lifecycle.GenericLifecycleObserver;
import androidx.lifecycle.Lifecycle;
import androidx.lifecycle.LifecycleOwner;
import androidx.paging.PagedList;
public class BoundaryCallbackMessages extends PagedList.BoundaryCallback<TupleMessageEx> {
private Context context;
private long fid;
private String search;
private Handler mainHandler;
private IBoundaryCallbackMessages intf;
private ExecutorService executor = Executors.newSingleThreadExecutor(Helper.backgroundThreadFactory);
private boolean enabled = false;
private IMAPStore istore = null;
private IMAPFolder ifolder = null;
private Message[] imessages = null;
interface IBoundaryCallbackMessages {
void onLoading();
void onLoaded();
void onError(Context context, Throwable ex);
}
BoundaryCallbackMessages(Context context, LifecycleOwner owner, long folder, String search, IBoundaryCallbackMessages intf) {
this.context = context;
this.fid = folder;
this.search = search;
this.mainHandler = new Handler(context.getMainLooper());
this.intf = intf;
owner.getLifecycle().addObserver(new GenericLifecycleObserver() {
@Override
public void onStateChanged(LifecycleOwner source, Lifecycle.Event event) {
if (event == Lifecycle.Event.ON_DESTROY)
new Thread(new Runnable() {
@Override
public void run() {
Log.i(Helper.TAG, "Boundary close");
try {
if (istore != null)
istore.close();
} catch (Throwable ex) {
Log.e(Helper.TAG, "Boundary " + ex + "\n" + Log.getStackTraceString(ex));
} finally {
istore = null;
ifolder = null;
imessages = null;
}
}
}).start();
}
});
}
void setEnabled(boolean enabled) {
this.enabled = enabled;
}
@Override
public void onItemAtEndLoaded(final TupleMessageEx itemAtEnd) {
Log.i(Helper.TAG, "onItemAtEndLoaded enabled=" + enabled);
if (!enabled)
return;
load(itemAtEnd.received);
}
void load(final long before) {
executor.submit(new Runnable() {
@Override
public void run() {
try {
mainHandler.post(new Runnable() {
@Override
public void run() {
intf.onLoading();
}
});
DB db = DB.getInstance(context);
EntityFolder folder = db.folder().getFolder(fid);
EntityAccount account = db.account().getAccount(folder.account);
if (imessages == null) {
// Refresh token
if (account.auth_type == Helper.AUTH_TYPE_GMAIL) {
account.password = Helper.refreshToken(context, "com.google", account.user, account.password);
db.account().setAccountPassword(account.id, account.password);
}
Properties props = MessageHelper.getSessionProperties(context, account.auth_type);
props.setProperty("mail.imap.throwsearchexception", "true");
Session isession = Session.getInstance(props, null);
Log.i(Helper.TAG, "Boundary connecting account=" + account.name);
istore = (IMAPStore) isession.getStore("imaps");
istore.connect(account.host, account.port, account.user, account.password);
Log.i(Helper.TAG, "Boundary opening folder=" + folder.name);
ifolder = (IMAPFolder) istore.getFolder(folder.name);
ifolder.open(Folder.READ_WRITE);
Log.i(Helper.TAG, "Boundary searching=" + search + " before=" + new Date(before));
imessages = ifolder.search(
new AndTerm(
new ReceivedDateTerm(ComparisonTerm.LT, new Date(before)),
new OrTerm(
new FromStringTerm(search),
new OrTerm(
new SubjectTerm(search),
new BodyTerm(search)))));
Log.i(Helper.TAG, "Boundary found messages=" + imessages.length);
}
int index = imessages.length - 1;
while (index >= 0) {
if (imessages[index].getReceivedDate().getTime() < before)
try {
Log.i(Helper.TAG, "Boundary sync uid=" + ifolder.getUID(imessages[index]));
ServiceSynchronize.synchronizeMessage(context, folder, ifolder, (IMAPMessage) imessages[index], true);
break;
} catch (Throwable ex) {
Log.e(Helper.TAG, "Boundary " + ex + "\n" + Log.getStackTraceString(ex));
}
index--;
}
EntityOperation.process(context); // download small attachments
Log.i(Helper.TAG, "Boundary done");
} catch (final Throwable ex) {
Log.e(Helper.TAG, "Boundary " + ex + "\n" + Log.getStackTraceString(ex));
mainHandler.post(new Runnable() {
@Override
public void run() {
intf.onError(context, ex);
}
});
} finally {
mainHandler.post(new Runnable() {
@Override
public void run() {
intf.onLoaded();
}
});
}
}
});
}
}

View File

@@ -45,7 +45,7 @@ import androidx.sqlite.db.SupportSQLiteDatabase;
// https://developer.android.com/topic/libraries/architecture/room.html
@Database(
version = 5,
version = 12,
entities = {
EntityIdentity.class,
EntityAccount.class,
@@ -54,6 +54,7 @@ import androidx.sqlite.db.SupportSQLiteDatabase;
EntityAttachment.class,
EntityOperation.class,
EntityAnswer.class,
EntityLog.class
}
)
@@ -73,6 +74,8 @@ public abstract class DB extends RoomDatabase {
public abstract DaoAnswer answer();
public abstract DaoLog log();
private static DB sInstance;
private static final String DB_NAME = "email";
@@ -143,6 +146,59 @@ public abstract class DB extends RoomDatabase {
db.execSQL("ALTER TABLE `identity` ADD COLUMN `auth_type` INTEGER NOT NULL DEFAULT 1");
}
})
.addMigrations(new Migration(5, 6) {
@Override
public void migrate(SupportSQLiteDatabase db) {
Log.i(Helper.TAG, "DB migration from version " + startVersion + " to " + endVersion);
db.execSQL("ALTER TABLE `message` ADD COLUMN `ui_found` INTEGER NOT NULL DEFAULT 0");
}
})
.addMigrations(new Migration(6, 7) {
@Override
public void migrate(SupportSQLiteDatabase db) {
Log.i(Helper.TAG, "DB migration from version " + startVersion + " to " + endVersion);
db.execSQL("CREATE TABLE IF NOT EXISTS `log` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `time` INTEGER NOT NULL, `data` TEXT NOT NULL)");
db.execSQL("CREATE INDEX `index_log_time` ON `log` (`time`)");
}
})
.addMigrations(new Migration(7, 8) {
@Override
public void migrate(SupportSQLiteDatabase db) {
Log.i(Helper.TAG, "DB migration from version " + startVersion + " to " + endVersion);
db.execSQL("CREATE INDEX `index_message_ui_found` ON `message` (`ui_found`)");
}
})
.addMigrations(new Migration(8, 9) {
@Override
public void migrate(SupportSQLiteDatabase db) {
Log.i(Helper.TAG, "DB migration from version " + startVersion + " to " + endVersion);
db.execSQL("ALTER TABLE `message` ADD COLUMN `headers` TEXT");
}
})
.addMigrations(new Migration(9, 10) {
@Override
public void migrate(SupportSQLiteDatabase db) {
Log.i(Helper.TAG, "DB migration from version " + startVersion + " to " + endVersion);
db.execSQL("ALTER TABLE `folder` ADD COLUMN `unified` INTEGER NOT NULL DEFAULT 0");
db.execSQL("CREATE INDEX `index_folder_unified` ON `folder` (`unified`)");
db.execSQL("UPDATE `folder` SET unified = 1 WHERE type = '" + EntityFolder.INBOX + "'");
}
})
.addMigrations(new Migration(10, 11) {
@Override
public void migrate(SupportSQLiteDatabase db) {
Log.i(Helper.TAG, "DB migration from version " + startVersion + " to " + endVersion);
db.execSQL("ALTER TABLE `account` ADD COLUMN `signature` TEXT");
}
})
.addMigrations(new Migration(11, 12) {
@Override
public void migrate(SupportSQLiteDatabase db) {
Log.i(Helper.TAG, "DB migration from version " + startVersion + " to " + endVersion);
db.execSQL("ALTER TABLE `message` ADD COLUMN `flagged` INTEGER NOT NULL DEFAULT 0");
db.execSQL("ALTER TABLE `message` ADD COLUMN `ui_flagged` INTEGER NOT NULL DEFAULT 0");
}
})
.build();
}

View File

@@ -61,7 +61,7 @@ public interface DaoFolder {
" JOIN account ON account.id = folder.account" +
" JOIN message ON message.folder = folder.id AND NOT message.ui_hide" +
" WHERE account.`synchronize`" +
" AND folder.type = '" + EntityFolder.INBOX + "'" +
" AND folder.unified" +
" GROUP BY folder.id")
LiveData<List<TupleFolderEx>> liveUnified();
@@ -114,8 +114,8 @@ public interface DaoFolder {
" AND type = :type")
int setFolderUser(long account, String type);
@Query("UPDATE folder SET synchronize = :synchronize, after = :after WHERE id = :id")
int setFolderProperties(long id, boolean synchronize, int after);
@Query("UPDATE folder SET synchronize = :synchronize, unified = :unified, after = :after WHERE id = :id")
int setFolderProperties(long id, boolean synchronize, boolean unified, int after);
@Query("DELETE FROM folder WHERE account= :account AND name = :name")
void deleteFolder(Long account, String name);

View File

@@ -48,8 +48,8 @@ public interface DaoIdentity {
@Query("SELECT * FROM identity WHERE id = :id")
LiveData<EntityIdentity> liveIdentity(long id);
@Query("SELECT * FROM identity WHERE account = :account AND `primary`")
EntityIdentity getPrimaryIdentity(long account);
@Query("SELECT COUNT(*) FROM identity WHERE synchronize")
int getSynchronizingIdentityCount();
@Insert
long insertIdentity(EntityIdentity identity);

View File

@@ -0,0 +1,47 @@
package eu.faircode.email;
/*
This file is part of FairEmail.
FairEmail is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
NetGuard is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with NetGuard. If not, see <http://www.gnu.org/licenses/>.
Copyright 2018 by Marcel Bokhorst (M66B)
*/
import java.util.List;
import androidx.lifecycle.LiveData;
import androidx.room.Dao;
import androidx.room.Insert;
import androidx.room.Query;
@Dao
public interface DaoLog {
@Query("SELECT * FROM log" +
" WHERE time > :from" +
" ORDER BY time DESC")
LiveData<List<EntityLog>> liveLogs(long from);
@Query("SELECT * FROM log" +
" WHERE time > :from" +
" ORDER BY time DESC")
List<EntityLog> getLogs(long from);
@Insert
long insertLog(EntityLog log);
@Query("DELETE FROM log" +
" WHERE time < :before")
int deleteLogs(long before);
}

View File

@@ -39,14 +39,14 @@ public interface DaoMessage {
", COUNT(message.id) as count" +
", SUM(CASE WHEN message.ui_seen THEN 0 ELSE 1 END) as unseen" +
", (SELECT COUNT(a.id) FROM attachment a WHERE a.message = message.id) AS attachments" +
", MAX(CASE WHEN folder.type = '" + EntityFolder.INBOX + "' THEN message.id ELSE 0 END) as dummy" +
", MAX(CASE WHEN folder.unified THEN message.id ELSE 0 END) as dummy" +
" FROM message" +
" JOIN account ON account.id = message.account" +
" JOIN folder ON folder.id = message.folder" +
" WHERE account.`synchronize`" +
" AND (NOT message.ui_hide OR :debug)" +
" GROUP BY CASE WHEN message.thread IS NULL THEN message.id ELSE message.thread END" +
" HAVING SUM(CASE WHEN folder.type = '" + EntityFolder.INBOX + "' THEN 1 ELSE 0 END) > 0" +
" HAVING SUM(unified) > 0" +
" ORDER BY message.received DESC")
DataSource.Factory<Integer, TupleMessageEx> pagedUnifiedInbox(boolean debug);
@@ -60,10 +60,11 @@ public interface DaoMessage {
" JOIN folder ON folder.id = message.folder" +
" LEFT JOIN folder f ON f.id = :folder" +
" WHERE (NOT message.ui_hide OR :debug)" +
" AND (NOT :found OR ui_found = :found)" +
" GROUP BY CASE WHEN message.thread IS NULL THEN message.id ELSE message.thread END" +
" HAVING SUM(CASE WHEN folder.id = :folder THEN 1 ELSE 0 END) > 0" +
" ORDER BY message.received DESC, message.sent DESC")
DataSource.Factory<Integer, TupleMessageEx> pagedFolder(long folder, boolean debug);
DataSource.Factory<Integer, TupleMessageEx> pagedFolder(long folder, boolean found, boolean debug);
@Query("SELECT message.*, account.name AS accountName, folder.name as folderName, folder.type as folderType" +
", 1 AS count" +
@@ -104,9 +105,15 @@ public interface DaoMessage {
" JOIN folder ON folder.id = message.folder" +
" WHERE message.account = :account" +
" AND message.thread = :thread" +
" AND folder.type <> '" + EntityFolder.OUTBOX + "'")
" AND folder.type <> '" + EntityFolder.OUTBOX + "'" +
" AND folder.type <> '" + EntityFolder.DRAFTS + "'")
List<EntityMessage> getMessageByThread(long account, String thread);
@Query("SELECT id FROM message" +
" WHERE folder = :folder" +
" ORDER BY message.received DESC, message.sent DESC")
List<Long> getMessageIDs(long folder);
@Query("SELECT message.*, account.name AS accountName, folder.name as folderName, folder.type as folderType" +
", (SELECT COUNT(m1.id) FROM message m1 WHERE m1.account = message.account AND m1.thread = message.thread AND NOT m1.ui_hide) AS count" +
", (SELECT COUNT(m2.id) FROM message m2 WHERE m2.account = message.account AND m2.thread = message.thread AND NOT m2.ui_hide AND NOT m2.ui_seen) AS unseen" +
@@ -121,13 +128,17 @@ public interface DaoMessage {
" JOIN account ON account.id = message.account" +
" JOIN folder ON folder.id = message.folder" +
" WHERE account.`synchronize`" +
" AND folder.type = '" + EntityFolder.INBOX + "'" +
" AND folder.unified" +
" AND NOT message.ui_seen AND NOT message.ui_hide" +
" AND (account.seen_until IS NULL OR message.stored > account.seen_until)" +
" ORDER BY message.received")
LiveData<List<EntityMessage>> liveUnseenUnified();
@Query("SELECT uid FROM message WHERE folder = :folder AND received >= :received AND NOT uid IS NULL")
@Query("SELECT uid FROM message" +
" WHERE folder = :folder" +
" AND received >= :received" +
" AND NOT uid IS NULL" +
" AND NOT ui_found" /* keep found messages */)
List<Long> getUids(long folder, long received);
@Insert
@@ -145,12 +156,27 @@ public interface DaoMessage {
@Query("UPDATE message SET ui_seen = :ui_seen WHERE id = :id")
int setMessageUiSeen(long id, boolean ui_seen);
@Query("UPDATE message SET flagged = :flagged WHERE id = :id")
int setMessageFlagged(long id, boolean flagged);
@Query("UPDATE message SET ui_flagged = :ui_flagged WHERE id = :id")
int setMessageUiFlagged(long id, boolean ui_flagged);
@Query("UPDATE message SET ui_hide = :ui_hide WHERE id = :id")
int setMessageUiHide(long id, boolean ui_hide);
@Query("UPDATE message SET error = :error WHERE id = :id")
int setMessageError(long id, String error);
@Query("UPDATE message SET ui_found = :found WHERE id = :id")
int setMessageFound(long id, boolean found);
@Query("UPDATE message SET ui_found = 0 WHERE folder = :folder")
int resetFound(long folder);
@Query("UPDATE message SET headers = :headers WHERE id = :id")
int setMessageHeaders(long id, String headers);
@Query("DELETE FROM message WHERE id = :id")
int deleteMessage(long id);
@@ -158,8 +184,11 @@ public interface DaoMessage {
int deleteMessage(long folder, long uid);
@Query("DELETE FROM message WHERE folder = :folder")
void deleteMessages(long folder);
int deleteMessages(long folder);
@Query("DELETE FROM message WHERE folder = :folder AND received < :received AND NOT uid IS NULL")
int deleteMessagesBefore(long folder, long received);
@Query("DELETE FROM message WHERE ui_found")
int deleteFoundMessages();
}

View File

@@ -34,6 +34,7 @@ public class EntityAccount {
@PrimaryKey(autoGenerate = true)
public Long id;
public String name;
public String signature;
@NonNull
public String host; // IMAP
@NonNull
@@ -51,7 +52,7 @@ public class EntityAccount {
@NonNull
public Boolean store_sent; // obsolete
@NonNull
public Integer poll_interval;
public Integer poll_interval; // NOOP interval
public Long seen_until;
public String state;
public String error;

View File

@@ -42,9 +42,11 @@ import static androidx.room.ForeignKey.CASCADE;
@Index(value = {"account", "name"}, unique = true),
@Index(value = {"account"}),
@Index(value = {"name"}),
@Index(value = {"type"})
@Index(value = {"type"}),
@Index(value = {"unified"})
}
)
public class EntityFolder implements Parcelable {
static final String TABLE_NAME = "folder";
@@ -56,6 +58,8 @@ public class EntityFolder implements Parcelable {
@NonNull
public String type;
@NonNull
public Boolean unified = false;
@NonNull
public Boolean synchronize;
@NonNull
public Integer after; // days

View File

@@ -0,0 +1,76 @@
package eu.faircode.email;
/*
This file is part of FairEmail.
FairEmail is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
NetGuard is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with NetGuard. If not, see <http://www.gnu.org/licenses/>.
Copyright 2018 by Marcel Bokhorst (M66B)
*/
import android.content.Context;
import java.util.Date;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import androidx.annotation.NonNull;
import androidx.room.Entity;
import androidx.room.Index;
import androidx.room.PrimaryKey;
@Entity(
tableName = EntityLog.TABLE_NAME,
foreignKeys = {
},
indices = {
@Index(value = {"time"})
}
)
public class EntityLog {
static final String TABLE_NAME = "log";
@PrimaryKey(autoGenerate = true)
public Long id;
@NonNull
public Long time;
@NonNull
public String data;
private static ExecutorService executor = Executors.newSingleThreadExecutor();
static void log(Context context, String data) {
final EntityLog entry = new EntityLog();
entry.time = new Date().getTime();
entry.data = data;
final DB db = DB.getInstance(context);
executor.submit(new Runnable() {
@Override
public void run() {
db.log().insertLog(entry);
}
});
}
@Override
public boolean equals(Object obj) {
if (obj instanceof EntityLog) {
EntityLog other = (EntityLog) obj;
return (this.time.equals(other.time) && this.data.equals(other.data));
} else
return false;
}
}

View File

@@ -63,7 +63,8 @@ import static androidx.room.ForeignKey.CASCADE;
@Index(value = {"thread"}),
@Index(value = {"received"}),
@Index(value = {"ui_seen"}),
@Index(value = {"ui_hide"})
@Index(value = {"ui_hide"}),
@Index(value = {"ui_found"})
}
)
public class EntityMessage implements Serializable {
@@ -86,6 +87,7 @@ public class EntityMessage implements Serializable {
public Address[] cc;
public Address[] bcc;
public Address[] reply;
public String headers;
public String subject;
public Long sent; // compose = null
@NonNull
@@ -95,15 +97,19 @@ public class EntityMessage implements Serializable {
@NonNull
public Boolean seen;
@NonNull
public Boolean flagged;
@NonNull
public Boolean ui_seen;
@NonNull
public Boolean ui_flagged;
@NonNull
public Boolean ui_hide;
@NonNull
public Boolean ui_found;
public String error;
@Ignore
String body = null;
@Ignore
boolean virtual = false;
static String generateMessageId() {
StringBuffer sb = new StringBuffer();
@@ -125,8 +131,9 @@ public class EntityMessage implements Serializable {
File file = getFile(context, id);
BufferedWriter out = null;
try {
this.body = (body == null ? "" : body);
out = new BufferedWriter(new FileWriter(file));
out.write(body == null ? "" : body);
out.write(this.body);
} finally {
if (out != null)
try {
@@ -184,12 +191,17 @@ public class EntityMessage implements Serializable {
equal(this.cc, other.cc) &&
equal(this.bcc, other.bcc) &&
equal(this.reply, other.reply) &&
(this.headers == null ? other.headers == null : this.headers.equals(other.headers)) &&
(this.subject == null ? other.subject == null : this.subject.equals(other.subject)) &&
(this.sent == null ? other.sent == null : this.sent.equals(other.sent)) &&
this.received.equals(other.received) &&
this.stored.equals(other.stored) &&
this.seen.equals(other.seen) &&
this.ui_seen.equals(other.ui_seen) &&
this.flagged.equals(other.flagged) &&
this.ui_flagged.equals(other.ui_flagged) &&
this.ui_hide.equals(other.ui_hide) &&
this.ui_found.equals(other.ui_found) &&
(this.error == null ? other.error == null : this.error.equals(other.error)));
}
return false;

View File

@@ -71,6 +71,8 @@ public class EntityOperation {
public static final String DELETE = "delete";
public static final String SEND = "send";
public static final String ATTACHMENT = "attachment";
public static final String HEADERS = "headers";
public static final String FLAG = "flag";
private static List<Intent> queue = new ArrayList<>();

View File

@@ -33,14 +33,21 @@ import android.widget.Toast;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
import javax.mail.Address;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.FragmentTransaction;
public class FragmentAbout extends FragmentEx {
private TextView tvVersion;
private Button btnLog;
private Button btnDebugInfo;
@Override
@Nullable
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
@@ -48,11 +55,21 @@ public class FragmentAbout extends FragmentEx {
View view = inflater.inflate(R.layout.fragment_about, container, false);
TextView tvVersion = view.findViewById(R.id.tvVersion);
final Button btnDebugInfo = view.findViewById(R.id.btnDebugInfo);
tvVersion = view.findViewById(R.id.tvVersion);
btnLog = view.findViewById(R.id.btnLog);
btnDebugInfo = view.findViewById(R.id.btnDebugInfo);
tvVersion.setText(getString(R.string.title_version, BuildConfig.VERSION_NAME));
btnLog.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
FragmentTransaction fragmentTransaction = getFragmentManager().beginTransaction();
fragmentTransaction.replace(R.id.content_frame, new FragmentLogs()).addToBackStack("logs");
fragmentTransaction.commit();
}
});
btnDebugInfo.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
@@ -80,13 +97,20 @@ public class FragmentAbout extends FragmentEx {
sb.append(String.format("Id: %s\r\n", Build.ID));
sb.append("\r\n");
// Get recent log
long from = new Date().getTime() - 12 * 3600 * 1000L;
DateFormat DF = SimpleDateFormat.getTimeInstance();
DB db = DB.getInstance(context);
for (EntityLog log : db.log().getLogs(from))
sb.append(DF.format(log.time)).append(" ").append(log.data).append("\r\n");
sb.append("\r\n");
sb.append(Helper.getLogcat());
String body = "<pre>" + sb.toString().replaceAll("\\r?\\n", "<br />") + "</pre>";
EntityMessage draft;
DB db = DB.getInstance(context);
try {
db.beginTransaction();
@@ -103,7 +127,10 @@ public class FragmentAbout extends FragmentEx {
draft.received = new Date().getTime();
draft.seen = false;
draft.ui_seen = false;
draft.flagged = false;
draft.ui_flagged = false;
draft.ui_hide = false;
draft.ui_found = false;
draft.id = db.message().insertMessage(draft);
draft.write(context, body);

View File

@@ -32,11 +32,10 @@ import android.content.pm.PackageManager;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.preference.PreferenceManager;
import android.text.Editable;
import android.text.Html;
import android.text.TextUtils;
import android.text.TextWatcher;
import android.text.method.LinkMovementMethod;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
@@ -51,9 +50,9 @@ import android.widget.ImageButton;
import android.widget.ProgressBar;
import android.widget.ScrollView;
import android.widget.Spinner;
import android.widget.TextView;
import android.widget.Toast;
import com.google.android.material.snackbar.Snackbar;
import com.google.android.material.textfield.TextInputLayout;
import com.sun.mail.imap.IMAPFolder;
import com.sun.mail.imap.IMAPStore;
@@ -75,26 +74,27 @@ import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.constraintlayout.widget.Group;
import androidx.core.content.ContextCompat;
import androidx.fragment.app.FragmentTransaction;
import androidx.lifecycle.Observer;
import static android.accounts.AccountManager.newChooseAccountIntent;
public class FragmentAccount extends FragmentEx {
private ViewGroup view;
private EditText etName;
private Spinner spProvider;
private EditText etHost;
private EditText etPort;
private Button btnAuthorize;
private EditText etUser;
private TextInputLayout tilPassword;
private TextView tvLink;
private Button btnAdvanced;
private EditText etName;
private EditText etSignature;
private ImageButton ibPro;
private CheckBox cbSynchronize;
private CheckBox cbPrimary;
private EditText etInterval;
private Button btnCheck;
private ProgressBar pbCheck;
private TextView tvIdle;
private Spinner spDrafts;
private Spinner spSent;
private Spinner spAll;
@@ -104,7 +104,9 @@ public class FragmentAccount extends FragmentEx {
private ProgressBar pbSave;
private ImageButton ibDelete;
private ProgressBar pbWait;
private Group grpInstructions;
private Group grpServer;
private Group grpAuthorize;
private Group grpAdvanced;
private Group grpFolders;
private long id = -1;
@@ -128,29 +130,40 @@ public class FragmentAccount extends FragmentEx {
// Get controls
spProvider = view.findViewById(R.id.spProvider);
etName = view.findViewById(R.id.etName);
etHost = view.findViewById(R.id.etHost);
btnAuthorize = view.findViewById(R.id.btnAuthorize);
etPort = view.findViewById(R.id.etPort);
btnAuthorize = view.findViewById(R.id.btnAuthorize);
etUser = view.findViewById(R.id.etUser);
tilPassword = view.findViewById(R.id.tilPassword);
tvLink = view.findViewById(R.id.tvLink);
btnAdvanced = view.findViewById(R.id.btnAdvanced);
etName = view.findViewById(R.id.etName);
etSignature = view.findViewById(R.id.etSignature);
ibPro = view.findViewById(R.id.ibPro);
cbSynchronize = view.findViewById(R.id.cbSynchronize);
cbPrimary = view.findViewById(R.id.cbPrimary);
etInterval = view.findViewById(R.id.etInterval);
btnCheck = view.findViewById(R.id.btnCheck);
pbCheck = view.findViewById(R.id.pbCheck);
tvIdle = view.findViewById(R.id.tvIdle);
spDrafts = view.findViewById(R.id.spDrafts);
spSent = view.findViewById(R.id.spSent);
spAll = view.findViewById(R.id.spAll);
spTrash = view.findViewById(R.id.spTrash);
spJunk = view.findViewById(R.id.spJunk);
btnSave = view.findViewById(R.id.btnSave);
pbSave = view.findViewById(R.id.pbSave);
ibDelete = view.findViewById(R.id.ibDelete);
pbWait = view.findViewById(R.id.pbWait);
grpInstructions = view.findViewById(R.id.grpInstructions);
grpServer = view.findViewById(R.id.grpServer);
grpAuthorize = view.findViewById(R.id.grpAuthorize);
grpAdvanced = view.findViewById(R.id.grpAdvanced);
grpFolders = view.findViewById(R.id.grpFolders);
// Wire controls
@@ -158,27 +171,32 @@ public class FragmentAccount extends FragmentEx {
spProvider.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
@Override
public void onItemSelected(AdapterView<?> adapterView, View view, int position, long id) {
Integer tag = (Integer) adapterView.getTag();
if (tag != null && tag.equals(position))
Provider provider = (Provider) adapterView.getSelectedItem();
grpServer.setVisibility(position == 1 ? View.VISIBLE : View.GONE);
grpAuthorize.setVisibility(position > 0 ? View.VISIBLE : View.GONE);
btnAuthorize.setVisibility(provider.type == null ? View.GONE : View.VISIBLE);
btnAdvanced.setVisibility(position > 0 ? View.VISIBLE : View.GONE);
if (position == 0)
grpAdvanced.setVisibility(View.GONE);
btnCheck.setVisibility(position > 0 ? View.VISIBLE : View.GONE);
grpFolders.setVisibility(View.GONE);
btnSave.setVisibility(View.GONE);
Object tag = adapterView.getTag();
if (tag != null && (Integer) tag == position)
return;
adapterView.setTag(position);
Provider provider = (Provider) adapterView.getSelectedItem();
etName.setText(provider.name);
btnAuthorize.setVisibility(provider.type == null ? View.GONE : View.VISIBLE);
if (authorized != null) {
authorized = null;
etUser.setText(null);
tilPassword.getEditText().setText(null);
}
etHost.setText(provider.imap_host);
etPort.setText(position == 0 ? null : Integer.toString(provider.imap_port));
etPort.setText(provider.imap_host == null ? null : Integer.toString(provider.imap_port));
tvLink.setText(Html.fromHtml("<a href=\"" + provider.link + "\">" + provider.link + "</a>"));
grpInstructions.setVisibility(provider.link == null ? View.GONE : View.VISIBLE);
etUser.setText(null);
tilPassword.getEditText().setText(null);
etName.setText(position > 1 ? provider.name : null);
}
@Override
@@ -215,12 +233,34 @@ public class FragmentAccount extends FragmentEx {
}
});
btnAdvanced.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
int visibility = (grpAdvanced.getVisibility() == View.VISIBLE ? View.GONE : View.VISIBLE);
grpAdvanced.setVisibility(visibility);
if (visibility == View.VISIBLE)
new Handler().post(new Runnable() {
@Override
public void run() {
((ScrollView) view).smoothScrollTo(0, btnCheck.getBottom());
}
});
}
});
ibPro.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
FragmentTransaction fragmentTransaction = getFragmentManager().beginTransaction();
fragmentTransaction.replace(R.id.content_frame, new FragmentPro()).addToBackStack("pro");
fragmentTransaction.commit();
}
});
cbSynchronize.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
@Override
public void onCheckedChanged(CompoundButton compoundButton, boolean checked) {
cbPrimary.setEnabled(checked);
btnCheck.setVisibility(checked ? View.VISIBLE : View.GONE);
btnSave.setVisibility(checked ? View.GONE : View.VISIBLE);
}
});
@@ -231,21 +271,18 @@ public class FragmentAccount extends FragmentEx {
btnAuthorize.setEnabled(false);
btnCheck.setEnabled(false);
pbCheck.setVisibility(View.VISIBLE);
btnSave.setVisibility(View.GONE);
grpFolders.setVisibility(View.GONE);
btnSave.setVisibility(View.GONE);
Provider provider = (Provider) spProvider.getSelectedItem();
Bundle args = new Bundle();
args.putLong("id", id);
args.putString("name", etName.getText().toString());
args.putString("host", etHost.getText().toString());
args.putString("port", etPort.getText().toString());
args.putString("user", etUser.getText().toString());
args.putString("password", tilPassword.getEditText().getText().toString());
args.putInt("auth_type", authorized == null ? Helper.AUTH_TYPE_PASSWORD : provider.getAuthType());
args.putBoolean("synchronize", cbSynchronize.isChecked());
args.putBoolean("primary", cbPrimary.isChecked());
new SimpleTask<List<EntityFolder>>() {
@Override
@@ -266,12 +303,6 @@ public class FragmentAccount extends FragmentEx {
if (TextUtils.isEmpty(password))
throw new Throwable(getContext().getString(R.string.title_no_password));
// Refresh token
if (id >= 0 && auth_type == Helper.AUTH_TYPE_GMAIL) {
password = Helper.refreshToken(getContext(), "com.google", user, password);
args.putString("password", password);
}
// Check IMAP server / get folders
List<EntityFolder> folders = new ArrayList<>();
Properties props = MessageHelper.getSessionProperties(context, auth_type);
@@ -282,11 +313,12 @@ public class FragmentAccount extends FragmentEx {
istore = (IMAPStore) isession.getStore("imaps");
istore.connect(host, Integer.parseInt(port), user, password);
if (!istore.hasCapability("IDLE"))
throw new MessagingException(getContext().getString(R.string.title_no_idle));
if (!istore.hasCapability("UIDPLUS"))
throw new MessagingException(getContext().getString(R.string.title_no_uidplus));
args.putBoolean("idle", istore.hasCapability("IDLE"));
for (Folder ifolder : istore.getDefaultFolder().list("*")) {
String type = null;
@@ -349,11 +381,6 @@ public class FragmentAccount extends FragmentEx {
btnCheck.setEnabled(true);
pbCheck.setVisibility(View.GONE);
// Refreshed token
tilPassword.getEditText().setText(args.getString("password"));
tvIdle.setVisibility(args.getBoolean("idle") ? View.GONE : View.VISIBLE);
final Collator collator = Collator.getInstance(Locale.getDefault());
collator.setStrength(Collator.SECONDARY); // Case insensitive, process accents etc
@@ -454,15 +481,15 @@ public class FragmentAccount extends FragmentEx {
Bundle args = new Bundle();
args.putLong("id", id);
args.putString("name", etName.getText().toString());
args.putString("host", etHost.getText().toString());
args.putString("port", etPort.getText().toString());
args.putString("user", etUser.getText().toString());
args.putString("password", tilPassword.getEditText().getText().toString());
args.putInt("auth_type", authorized == null ? Helper.AUTH_TYPE_PASSWORD : provider.getAuthType());
args.putBoolean("synchronize", cbSynchronize.isChecked());
args.putString("name", etName.getText().toString());
args.putString("signature", etSignature.getText().toString());
args.putBoolean("primary", cbPrimary.isChecked());
args.putString("poll_interval", etInterval.getText().toString());
args.putParcelable("drafts", drafts);
args.putParcelable("sent", sent);
args.putParcelable("all", all);
@@ -472,15 +499,15 @@ public class FragmentAccount extends FragmentEx {
new SimpleTask<Void>() {
@Override
protected Void onLoad(Context context, Bundle args) throws Throwable {
String name = args.getString("name");
String host = args.getString("host");
String port = args.getString("port");
String user = args.getString("user");
String password = args.getString("password");
int auth_type = args.getInt("auth_type");
String name = args.getString("name");
String signature = args.getString("signature");
boolean synchronize = args.getBoolean("synchronize");
boolean primary = args.getBoolean("primary");
String poll_interval = args.getString("poll_interval");
EntityFolder drafts = args.getParcelable("drafts");
EntityFolder sent = args.getParcelable("sent");
EntityFolder all = args.getParcelable("all");
@@ -498,9 +525,6 @@ public class FragmentAccount extends FragmentEx {
if (synchronize && drafts == null)
throw new Throwable(getContext().getString(R.string.title_no_drafts));
if (TextUtils.isEmpty(poll_interval))
poll_interval = "9";
// Check IMAP server
if (synchronize) {
Session isession = Session.getInstance(MessageHelper.getSessionProperties(context, auth_type), null);
@@ -530,6 +554,7 @@ public class FragmentAccount extends FragmentEx {
if (account == null)
account = new EntityAccount();
account.name = name;
account.signature = signature;
account.host = host;
account.port = Integer.parseInt(port);
account.user = user;
@@ -538,7 +563,7 @@ public class FragmentAccount extends FragmentEx {
account.synchronize = synchronize;
account.primary = (account.synchronize && primary);
account.store_sent = false;
account.poll_interval = Integer.parseInt(poll_interval);
account.poll_interval = 9;
if (!synchronize)
account.error = null;
@@ -557,6 +582,7 @@ public class FragmentAccount extends FragmentEx {
inbox.name = "INBOX";
inbox.type = EntityFolder.INBOX;
inbox.synchronize = true;
inbox.unified = true;
inbox.after = EntityFolder.DEFAULT_INBOX_SYNC;
folders.add(inbox);
@@ -670,16 +696,22 @@ public class FragmentAccount extends FragmentEx {
Helper.setViewsEnabled(view, false);
btnAuthorize.setVisibility(View.GONE);
tilPassword.setPasswordVisibilityToggleEnabled(id < 0);
tvLink.setMovementMethod(LinkMovementMethod.getInstance());
btnAuthorize.setEnabled(false);
btnCheck.setEnabled(false);
btnAdvanced.setVisibility(View.GONE);
btnCheck.setVisibility(View.GONE);
pbCheck.setVisibility(View.GONE);
btnSave.setVisibility(View.GONE);
pbSave.setVisibility(View.GONE);
tvIdle.setVisibility(View.GONE);
grpFolders.setVisibility(View.GONE);
ibDelete.setVisibility(View.GONE);
grpServer.setVisibility(View.GONE);
grpAuthorize.setVisibility(View.GONE);
grpAdvanced.setVisibility(View.GONE);
grpFolders.setVisibility(View.GONE);
return view;
}
@@ -689,6 +721,7 @@ public class FragmentAccount extends FragmentEx {
outState.putInt("provider", spProvider.getSelectedItemPosition());
outState.putString("authorized", authorized);
outState.putString("password", tilPassword.getEditText().getText().toString());
outState.putInt("advanced", grpAdvanced.getVisibility());
}
@Override
@@ -697,46 +730,66 @@ public class FragmentAccount extends FragmentEx {
// Observe
DB.getInstance(getContext()).account().liveAccount(id).observe(getViewLifecycleOwner(), new Observer<EntityAccount>() {
boolean once = false;
private boolean once = false;
@Override
public void onChanged(@Nullable EntityAccount account) {
if (once)
return;
once = true;
// Get providers
List<Provider> providers = Provider.loadProfiles(getContext());
providers.add(0, new Provider(getString(R.string.title_custom)));
providers.add(0, new Provider(getString(R.string.title_select)));
providers.add(1, new Provider(getString(R.string.title_custom)));
ArrayAdapter<Provider> padapter = new ArrayAdapter<>(getContext(), R.layout.spinner_item, providers);
padapter.setDropDownViewResource(R.layout.spinner_dropdown_item);
spProvider.setAdapter(padapter);
if (savedInstanceState == null) {
if (once)
return;
once = true;
spProvider.setTag(0);
spProvider.setSelection(0);
if (account != null) {
for (int pos = 1; pos < providers.size(); pos++)
if (providers.get(pos).imap_host.equals(account.host)) {
boolean found = false;
for (int pos = 2; pos < providers.size(); pos++) {
Provider provider = providers.get(pos);
if (provider.imap_host.equals(account.host) &&
provider.imap_port == account.port) {
found = true;
spProvider.setTag(pos);
spProvider.setSelection(pos);
break;
}
}
if (!found) {
spProvider.setTag(1);
spProvider.setSelection(1);
}
etHost.setText(account.host);
etPort.setText(Long.toString(account.port));
}
etName.setText(account == null ? null : account.name);
etHost.setText(account == null ? null : account.host);
etPort.setText(account == null ? null : Long.toString(account.port));
authorized = (account != null && account.auth_type != Helper.AUTH_TYPE_PASSWORD ? account.password : null);
etUser.setText(account == null ? null : account.user);
tilPassword.getEditText().setText(account == null ? null : account.password);
etName.setText(account == null ? null : account.name);
etSignature.setText(account == null ? null : account.signature);
cbSynchronize.setChecked(account == null ? true : account.synchronize);
cbPrimary.setChecked(account == null ? true : account.primary);
etInterval.setText(account == null ? "9" : Integer.toString(account.poll_interval));
if (account == null)
new SimpleTask<Integer>() {
@Override
protected Integer onLoad(Context context, Bundle args) {
return DB.getInstance(context).account().getSynchronizingAccountCount();
}
@Override
protected void onLoaded(Bundle args, Integer count) {
cbPrimary.setChecked(count == 0);
}
}.load(FragmentAccount.this, new Bundle());
} else {
int provider = savedInstanceState.getInt("provider");
spProvider.setTag(provider);
@@ -744,24 +797,25 @@ public class FragmentAccount extends FragmentEx {
authorized = savedInstanceState.getString("authorized");
tilPassword.getEditText().setText(savedInstanceState.getString("password"));
grpAdvanced.setVisibility(savedInstanceState.getInt("advanced"));
}
Helper.setViewsEnabled(view, true);
Provider provider = (Provider) spProvider.getSelectedItem();
btnAuthorize.setVisibility(provider.getAuthType() == Helper.AUTH_TYPE_PASSWORD ? View.GONE : View.VISIBLE);
tvLink.setText(Html.fromHtml("<a href=\"" + provider.link + "\">" + provider.link + "</a>"));
grpInstructions.setVisibility(provider.link == null ? View.GONE : View.VISIBLE);
boolean pro = PreferenceManager.getDefaultSharedPreferences(getContext()).getBoolean("pro", false);
etSignature.setHint(pro ? R.string.title_optional : R.string.title_pro_feature);
etSignature.setEnabled(pro);
if (pro) {
ViewGroup.LayoutParams lp = ibPro.getLayoutParams();
lp.height = 0;
lp.width = 0;
ibPro.setLayoutParams(lp);
}
cbPrimary.setEnabled(cbSynchronize.isChecked());
btnCheck.setVisibility(cbSynchronize.isChecked() ? View.VISIBLE : View.GONE);
btnSave.setVisibility(cbSynchronize.isChecked() ? View.GONE : View.VISIBLE);
// Consider previous check/save/delete as cancelled
ibDelete.setVisibility(account == null ? View.GONE : View.VISIBLE);
btnAuthorize.setEnabled(true);
btnCheck.setEnabled(true);
pbWait.setVisibility(View.GONE);
}
});
@@ -801,6 +855,8 @@ public class FragmentAccount extends FragmentEx {
Log.i(Helper.TAG, "Accounts=" + accounts.length);
for (final Account account : accounts)
if (name.equals(account.name)) {
final Snackbar snackbar = Snackbar.make(view, R.string.title_authorizing, Snackbar.LENGTH_SHORT);
snackbar.show();
am.getAuthToken(
account,
Helper.getAuthTokenType(type),
@@ -820,6 +876,8 @@ public class FragmentAccount extends FragmentEx {
} catch (Throwable ex) {
Log.e(Helper.TAG, ex + "\n" + Log.getStackTraceString(ex));
Toast.makeText(getContext(), Helper.formatThrowable(ex), Toast.LENGTH_LONG).show();
} finally {
snackbar.dismiss();
}
}
},

View File

@@ -72,7 +72,7 @@ public class FragmentAnswer extends FragmentEx {
@Override
public boolean onNavigationItemSelected(MenuItem menuItem) {
switch (menuItem.getItemId()) {
case R.id.action_trash:
case R.id.action_delete:
onActionTrash();
return true;
case R.id.action_save:
@@ -99,7 +99,7 @@ public class FragmentAnswer extends FragmentEx {
public void onChanged(EntityAnswer answer) {
etName.setText(answer == null ? null : answer.name);
etText.setText(answer == null ? null : answer.text);
bottom_navigation.findViewById(R.id.action_trash).setVisibility(answer == null ? View.GONE : View.VISIBLE);
bottom_navigation.findViewById(R.id.action_delete).setVisibility(answer == null ? View.GONE : View.VISIBLE);
pbWait.setVisibility(View.GONE);
grpReady.setVisibility(View.VISIBLE);

View File

@@ -20,8 +20,8 @@ package eu.faircode.email;
*/
import android.Manifest;
import android.app.PendingIntent;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.database.Cursor;
@@ -56,19 +56,14 @@ import android.widget.Toast;
import com.google.android.material.bottomnavigation.BottomNavigationView;
import com.google.android.material.snackbar.Snackbar;
import org.openintents.openpgp.OpenPgpError;
import org.openintents.openpgp.util.OpenPgpApi;
import org.openintents.openpgp.util.OpenPgpServiceConnection;
import java.io.BufferedOutputStream;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
@@ -81,6 +76,7 @@ import javax.mail.internet.InternetAddress;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.constraintlayout.widget.Group;
import androidx.core.content.ContextCompat;
import androidx.cursoradapter.widget.SimpleCursorAdapter;
@@ -118,27 +114,6 @@ public class FragmentCompose extends FragmentEx {
private boolean addresses;
private boolean autosave = true;
private String encrypted = null;
private OpenPgpServiceConnection openPgpConnection = null;
private static final int ATTACHMENT_BUFFER_SIZE = 8192; // bytes
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
openPgpConnection = new OpenPgpServiceConnection(getContext(), "org.sufficientlysecure.keychain");
openPgpConnection.bindToService();
}
@Override
public void onDestroy() {
super.onDestroy();
if (openPgpConnection != null) {
openPgpConnection.unbindFromService();
openPgpConnection = null;
}
}
@Override
@Nullable
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
@@ -253,8 +228,21 @@ public class FragmentCompose extends FragmentEx {
bottom_navigation.setOnNavigationItemSelectedListener(new BottomNavigationView.OnNavigationItemSelectedListener() {
@Override
public boolean onNavigationItemSelected(@NonNull MenuItem item) {
onAction(item.getItemId());
int action = item.getItemId();
if (action == R.id.action_delete) {
AlertDialog.Builder builder = new AlertDialog.Builder(getContext());
builder
.setMessage(R.string.title_ask_delete)
.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
onAction(R.id.action_delete);
}
})
.setNegativeButton(android.R.string.cancel, null).show();
} else
onAction(action);
return false;
}
});
@@ -319,7 +307,7 @@ public class FragmentCompose extends FragmentEx {
public CharSequence convertToString(Cursor cursor) {
int colName = cursor.getColumnIndex(ContactsContract.Contacts.DISPLAY_NAME);
int colEmail = cursor.getColumnIndex(ContactsContract.CommonDataKinds.Email.DATA);
return cursor.getString(colName) + "<" + cursor.getString(colEmail) + ">";
return cursor.getString(colName) + " <" + cursor.getString(colEmail) + ">";
}
});
}
@@ -327,6 +315,7 @@ public class FragmentCompose extends FragmentEx {
rvAttachment.setHasFixedSize(false);
LinearLayoutManager llm = new LinearLayoutManager(getContext());
rvAttachment.setLayoutManager(llm);
rvAttachment.setItemAnimator(null);
adapter = new AdapterAttachment(getContext(), getViewLifecycleOwner(), false);
rvAttachment.setAdapter(adapter);
@@ -344,7 +333,6 @@ public class FragmentCompose extends FragmentEx {
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putLong("working", working);
outState.putString("encrypted", encrypted);
}
@Override
@@ -376,11 +364,10 @@ public class FragmentCompose extends FragmentEx {
draftLoader.load(this, args);
}
} else {
encrypted = savedInstanceState.getString("encrypted");
long id = savedInstanceState.getLong("working");
Bundle args = new Bundle();
args.putString("action", "edit");
args.putLong("id", savedInstanceState.getLong("working"));
args.putLong("id", id);
args.putLong("account", -1);
args.putLong("reference", -1);
args.putLong("answer", -1);
@@ -407,7 +394,6 @@ public class FragmentCompose extends FragmentEx {
menu.findItem(R.id.menu_attachment).setVisible(!free && working >= 0);
menu.findItem(R.id.menu_attachment).setEnabled(etBody.isEnabled());
menu.findItem(R.id.menu_addresses).setVisible(!free && working >= 0);
menu.findItem(R.id.menu_encrypt).setVisible(encrypted == null);
}
@Override
@@ -419,9 +405,6 @@ public class FragmentCompose extends FragmentEx {
case R.id.menu_addresses:
onMenuAddresses();
return true;
case R.id.menu_encrypt:
onMenuEncrypt();
return true;
default:
return super.onOptionsItemSelected(item);
}
@@ -438,81 +421,6 @@ public class FragmentCompose extends FragmentEx {
grpAddresses.setVisibility(grpAddresses.getVisibility() == View.GONE ? View.VISIBLE : View.GONE);
}
private void onMenuEncrypt() {
Log.i(Helper.TAG, "On encrypt");
if (!PreferenceManager.getDefaultSharedPreferences(getContext()).getBoolean("pro", false)) {
FragmentTransaction fragmentTransaction = getFragmentManager().beginTransaction();
fragmentTransaction.replace(R.id.content_frame, new FragmentPro()).addToBackStack("pro");
fragmentTransaction.commit();
return;
}
try {
if (openPgpConnection == null || !openPgpConnection.isBound())
throw new IllegalArgumentException(getString(R.string.title_no_openpgp));
EntityIdentity identity = (EntityIdentity) spFrom.getSelectedItem();
if (identity == null)
throw new IllegalArgumentException(getString(R.string.title_from_missing));
Intent data = new Intent();
data.setAction(OpenPgpApi.ACTION_ENCRYPT);
data.putExtra(OpenPgpApi.EXTRA_USER_IDS, new String[]{identity.email});
data.putExtra(OpenPgpApi.EXTRA_REQUEST_ASCII_ARMOR, true);
String plain = etBody.getText().toString();
final InputStream is = new ByteArrayInputStream(plain.getBytes("UTF-8"));
final ByteArrayOutputStream os = new ByteArrayOutputStream();
OpenPgpApi api = new OpenPgpApi(getContext(), openPgpConnection.getService());
api.executeApiAsync(data, is, os, new OpenPgpApi.IOpenPgpCallback() {
@Override
public void onReturn(Intent result) {
try {
int code = result.getIntExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_ERROR);
switch (code) {
case OpenPgpApi.RESULT_CODE_SUCCESS: {
Log.i(Helper.TAG, "Encrypted");
FragmentCompose.this.encrypted = os.toString("UTF-8");
getActivity().invalidateOptionsMenu();
etBody.setText(FragmentCompose.this.encrypted);
break;
}
case OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED: {
Log.i(Helper.TAG, "User interaction");
PendingIntent pi = result.getParcelableExtra(OpenPgpApi.RESULT_INTENT);
startIntentSenderForResult(
pi.getIntentSender(),
ActivityCompose.REQUEST_OPENPGP,
null, 0, 0, 0,
new Bundle());
break;
}
case OpenPgpApi.RESULT_CODE_ERROR: {
OpenPgpError error = result.getParcelableExtra(OpenPgpApi.RESULT_ERROR);
throw new IllegalArgumentException(error.getMessage());
}
}
} catch (Throwable ex) {
Log.e(Helper.TAG, ex + "\n" + Log.getStackTraceString(ex));
if (ex instanceof IllegalArgumentException)
Snackbar.make(view, ex.getMessage(), Snackbar.LENGTH_LONG).show();
else
Toast.makeText(getContext(), ex.toString(), Toast.LENGTH_LONG).show();
}
}
});
} catch (Throwable ex) {
Log.e(Helper.TAG, ex + "\n" + Log.getStackTraceString(ex));
if (ex instanceof IllegalArgumentException)
Snackbar.make(view, ex.getMessage(), Snackbar.LENGTH_LONG).show();
else
Toast.makeText(getContext(), ex.toString(), Toast.LENGTH_LONG).show();
}
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
Log.i(Helper.TAG, "Compose onActivityResult request=" + requestCode + " result=" + resultCode + " data=" + data);
@@ -520,11 +428,10 @@ public class FragmentCompose extends FragmentEx {
if (requestCode == ActivityCompose.REQUEST_ATTACHMENT) {
if (data != null)
handleAddAttachment(data);
} else if (requestCode == ActivityCompose.REQUEST_OPENPGP) {
Log.i(Helper.TAG, "User interacted");
onMenuEncrypt();
} else
handlePickContact(requestCode, data);
} else {
if (data != null)
handlePickContact(requestCode, data);
}
}
}
@@ -679,7 +586,7 @@ public class FragmentCompose extends FragmentEx {
os = new BufferedOutputStream(new FileOutputStream(file));
int size = 0;
byte[] buffer = new byte[ATTACHMENT_BUFFER_SIZE];
byte[] buffer = new byte[Helper.ATTACHMENT_BUFFER_SIZE];
for (int len = is.read(buffer); len != -1; len = is.read(buffer)) {
size += len;
os.write(buffer, 0, len);
@@ -715,11 +622,11 @@ public class FragmentCompose extends FragmentEx {
protected EntityMessage onLoad(Context context, Bundle args) throws IOException {
String action = args.getString("action");
long id = args.getLong("id", -1);
long account = args.getLong("account", -1);
long reference = args.getLong("reference", -1);
long answer = args.getLong("answer", -1);
boolean pro = PreferenceManager.getDefaultSharedPreferences(context).getBoolean("pro", false);
Log.i(Helper.TAG, "Load draft action=" + action + " id=" + id + " account=" + account + " reference=" + reference);
Log.i(Helper.TAG, "Load draft action=" + action + " id=" + id + " reference=" + reference);
EntityMessage draft;
@@ -734,34 +641,39 @@ public class FragmentCompose extends FragmentEx {
} else
return draft;
EntityAccount account;
EntityMessage ref = db.message().getMessage(reference);
if (ref == null) {
if (account < 0) {
EntityAccount a = db.account().getPrimaryAccount();
if (a == null)
long aid = args.getLong("account", -1);
if (aid < 0) {
account = db.account().getPrimaryAccount();
if (account == null)
throw new IllegalArgumentException(context.getString(R.string.title_no_account));
account = a.id;
}
} else
account = db.account().getAccount(aid);
} else {
account = ref.account;
account = db.account().getAccount(ref.account);
// Reply to sender, not to known self
if (ref.from != null && ref.from.length == 1) {
// All identities, synchronized or not
// Reply to recipient, not to known self
if (ref.from != null && ref.from.length > 0) {
String from = Helper.canonicalAddress(((InternetAddress) ref.from[0]).getAddress());
List<EntityIdentity> identities = db.identity().getIdentities();
for (EntityIdentity identity : identities)
if (((InternetAddress) ref.from[0]).getAddress().equals(identity.email)) {
for (EntityIdentity identity : identities) {
String email = Helper.canonicalAddress(identity.email);
if (from.equals(email)) {
Log.i(Helper.TAG, "Swapping from/to");
Address[] tmp = ref.to;
ref.to = ref.from;
ref.reply = null;
ref.from = tmp;
break;
}
}
}
}
EntityFolder drafts;
drafts = db.folder().getFolderByType(account, EntityFolder.DRAFTS);
drafts = db.folder().getFolderByType(account.id, EntityFolder.DRAFTS);
if (drafts == null)
drafts = db.folder().getPrimaryDrafts();
if (drafts == null)
@@ -770,7 +682,7 @@ public class FragmentCompose extends FragmentEx {
String body = "";
draft = new EntityMessage();
draft.account = account;
draft.account = account.id;
draft.folder = drafts.id;
draft.msgid = EntityMessage.generateMessageId(); // for multiple appends
@@ -798,8 +710,13 @@ public class FragmentCompose extends FragmentEx {
draft.subject = args.getString("subject");
body = args.getString("body");
if (!TextUtils.isEmpty(body))
body = "<pre>" + body.replaceAll("\\r?\\n", "<br />") + "</pre>";
if (body == null)
body = "";
else
body = body.replaceAll("\\r?\\n", "<br />");
if (pro && !TextUtils.isEmpty(account.signature))
body = "<br>" + account.signature.replaceAll("\\r?\\n", "<br />") + "<br>" + body;
} else {
draft.thread = ref.thread;
@@ -808,8 +725,24 @@ public class FragmentCompose extends FragmentEx {
draft.replying = ref.id;
draft.to = (ref.reply == null || ref.reply.length == 0 ? ref.from : ref.reply);
draft.from = ref.to;
if ("reply_all".equals(action))
draft.cc = ref.cc;
if ("reply_all".equals(action)) {
List<Address> addresses = new ArrayList<>();
if (ref.to != null)
addresses.addAll(Arrays.asList(ref.to));
if (ref.cc != null)
addresses.addAll(Arrays.asList(ref.cc));
List<EntityIdentity> identities = db.identity().getIdentities();
for (Address address : new ArrayList<>(addresses)) {
String cc = Helper.canonicalAddress(((InternetAddress) address).getAddress());
for (EntityIdentity identity : identities) {
String email = Helper.canonicalAddress(identity.email);
if (cc.equals(email))
addresses.remove(address);
}
}
draft.cc = addresses.toArray(new Address[0]);
}
} else if ("forward".equals(action)) {
//msg.replying = ref.id;
@@ -818,8 +751,14 @@ public class FragmentCompose extends FragmentEx {
if ("reply".equals(action) || "reply_all".equals(action)) {
String text = "";
if (answer > 0)
if (answer > 0) {
text = db.answer().getAnswer(answer).text;
String name = "";
if (draft.to != null && draft.to.length > 0)
name = ((InternetAddress) draft.to[0]).getPersonal();
text = text.replace("$name$", name);
}
draft.subject = context.getString(R.string.title_subject_reply, ref.subject);
body = String.format("%s<br><br>%s %s:<br><br>%s",
text.replaceAll("\\r?\\n", "<br />"),
@@ -833,12 +772,18 @@ public class FragmentCompose extends FragmentEx {
Html.escapeHtml(MessageHelper.getFormattedAddresses(ref.from, true)),
HtmlHelper.sanitize(context, ref.read(context), true));
}
if (pro && !TextUtils.isEmpty(account.signature))
body = "<br>" + account.signature.replaceAll("\\r?\\n", "<br />") + "<br>" + body;
}
draft.received = new Date().getTime();
draft.seen = false;
draft.ui_seen = false;
draft.flagged = false;
draft.ui_flagged = false;
draft.ui_hide = false;
draft.ui_found = false;
draft.id = db.message().insertMessage(draft);
draft.write(context, body == null ? "" : body);
@@ -881,14 +826,11 @@ public class FragmentCompose extends FragmentEx {
@Override
protected Spanned onLoad(Context context, Bundle args) throws Throwable {
String body = EntityMessage.read(context, args.getLong("id"));
if (body != null && body.startsWith("-----BEGIN PGP MESSAGE-----"))
args.putString("encrypted", body);
return Html.fromHtml(body);
}
@Override
protected void onLoaded(Bundle args, Spanned body) {
FragmentCompose.this.encrypted = args.getString("encrypted");
getActivity().invalidateOptionsMenu();
etBody.setText(body);
etBody.setSelection(0);
@@ -942,9 +884,9 @@ public class FragmentCompose extends FragmentEx {
// Select identity matching from address
if (!found && draft.from != null && draft.from.length > 0) {
String from = Helper.canonicalAddress(((InternetAddress) draft.from[0]).getAddress());
for (int pos = 0; pos < identities.size(); pos++) {
if (Helper.canonicalAddress(identities.get(pos).email).equals(from)) {
String email = Helper.canonicalAddress(identities.get(pos).email);
if (email.equals(from)) {
spFrom.setSelection(pos);
found = true;
break;
@@ -1025,10 +967,10 @@ public class FragmentCompose extends FragmentEx {
Log.i(Helper.TAG, "Load action id=" + draft.id + " action=" + action);
// Convert data
Address afrom[] = (identity == null ? null : new Address[]{new InternetAddress(identity.email, identity.name)});
Address ato[] = (TextUtils.isEmpty(to) ? null : InternetAddress.parse(to));
Address acc[] = (TextUtils.isEmpty(cc) ? null : InternetAddress.parse(cc));
Address abcc[] = (TextUtils.isEmpty(bcc) ? null : InternetAddress.parse(bcc));
InternetAddress afrom[] = (identity == null ? null : new InternetAddress[]{new InternetAddress(identity.email, identity.name)});
InternetAddress ato[] = (TextUtils.isEmpty(to) ? null : InternetAddress.parse(to));
InternetAddress acc[] = (TextUtils.isEmpty(cc) ? null : InternetAddress.parse(cc));
InternetAddress abcc[] = (TextUtils.isEmpty(bcc) ? null : InternetAddress.parse(bcc));
// Update draft
draft.identity = (identity == null ? null : identity.id);
@@ -1039,31 +981,17 @@ public class FragmentCompose extends FragmentEx {
draft.subject = subject;
draft.received = new Date().getTime();
String pbody;
if (encrypted == null)
pbody = "<pre>" + body.replaceAll("\\r?\\n", "<br />") + "</pre>";
else
pbody = encrypted;
String pbody = "<pre>" + body.replaceAll("\\r?\\n", "<br />") + "</pre>";
// Execute action
if (action == R.id.action_trash) {
draft.ui_seen = true;
if (action == R.id.action_delete) {
draft.msgid = null;
draft.ui_hide = true;
db.message().updateMessage(draft);
draft.write(context, pbody);
EntityFolder trash = db.folder().getFolderByType(draft.account, EntityFolder.TRASH);
EntityOperation.queue(db, draft, EntityOperation.MOVE, trash.id);
EntityOperation.queue(db, draft, EntityOperation.DELETE);
} else if (action == R.id.action_save) {
EntityIdentity primary = db.identity().getPrimaryIdentity(draft.account);
if ((primary == null || draft.identity == primary.id) &&
ato == null && acc == null && abcc == null &&
TextUtils.isEmpty(subject) &&
TextUtils.isEmpty(body) &&
db.attachment().getAttachmentCount(draft.id) == 0)
return null;
db.message().updateMessage(draft);
draft.write(context, pbody);
@@ -1134,10 +1062,10 @@ public class FragmentCompose extends FragmentEx {
Helper.setViewsEnabled(view, true);
getActivity().invalidateOptionsMenu();
if (action == R.id.action_trash) {
if (action == R.id.action_delete) {
autosave = false;
getFragmentManager().popBackStack();
Toast.makeText(getContext(), R.string.title_draft_trashed, Toast.LENGTH_LONG).show();
Toast.makeText(getContext(), R.string.title_draft_deleted, Toast.LENGTH_LONG).show();
} else if (action == R.id.action_save) {
if (draft != null)
@@ -1166,7 +1094,7 @@ public class FragmentCompose extends FragmentEx {
private Context context;
private List<EntityIdentity> identities;
public IdentityAdapter(@NonNull Context context, List<EntityIdentity> identities) {
IdentityAdapter(@NonNull Context context, List<EntityIdentity> identities) {
super(context, 0, identities);
this.context = context;
this.identities = identities;
@@ -1183,10 +1111,8 @@ public class FragmentCompose extends FragmentEx {
return getLayout(position, convertView, parent);
}
public View getLayout(int position, View convertView, ViewGroup parent) {
View view = convertView;
if (view == convertView)
view = LayoutInflater.from(context).inflate(R.layout.spinner_item2, parent, false);
View getLayout(int position, View convertView, ViewGroup parent) {
View view = LayoutInflater.from(context).inflate(R.layout.spinner_item2, parent, false);
EntityIdentity identity = identities.get(position);

View File

@@ -38,6 +38,7 @@ import androidx.lifecycle.Observer;
public class FragmentFolder extends FragmentEx {
private ViewGroup view;
private CheckBox cbSynchronize;
private CheckBox cbUnified;
private EditText etAfter;
private Button btnSave;
private ProgressBar pbSave;
@@ -63,6 +64,7 @@ public class FragmentFolder extends FragmentEx {
// Get controls
cbSynchronize = view.findViewById(R.id.cbSynchronize);
cbUnified = view.findViewById(R.id.cbUnified);
etAfter = view.findViewById(R.id.etAfter);
pbSave = view.findViewById(R.id.pbSave);
btnSave = view.findViewById(R.id.btnSave);
@@ -78,6 +80,7 @@ public class FragmentFolder extends FragmentEx {
Bundle args = new Bundle();
args.putLong("id", id);
args.putBoolean("synchronize", cbSynchronize.isChecked());
args.putBoolean("unified", cbUnified.isChecked());
args.putString("after", etAfter.getText().toString());
new SimpleTask<Void>() {
@@ -85,6 +88,7 @@ public class FragmentFolder extends FragmentEx {
protected Void onLoad(Context context, Bundle args) {
long id = args.getLong("id");
boolean synchronize = args.getBoolean("synchronize");
boolean unified = args.getBoolean("unified");
String after = args.getString("after");
int days = (TextUtils.isEmpty(after) ? 7 : Integer.parseInt(after));
@@ -92,7 +96,7 @@ public class FragmentFolder extends FragmentEx {
try {
db.beginTransaction();
db.folder().setFolderProperties(id, synchronize, days);
db.folder().setFolderProperties(id, synchronize, unified, days);
if (!synchronize)
db.folder().setFolderError(id, null);
@@ -142,7 +146,7 @@ public class FragmentFolder extends FragmentEx {
// Observe
DB.getInstance(getContext()).folder().liveFolder(id).observe(getViewLifecycleOwner(), new Observer<EntityFolder>() {
boolean once = false;
private boolean once = false;
@Override
public void onChanged(@Nullable EntityFolder folder) {
@@ -151,12 +155,13 @@ public class FragmentFolder extends FragmentEx {
return;
}
if (savedInstanceState == null) {
if (once)
return;
once = true;
if (once)
return;
once = true;
if (savedInstanceState == null) {
cbSynchronize.setChecked(folder.synchronize);
cbUnified.setChecked(folder.unified);
etAfter.setText(Integer.toString(folder.after));
}

View File

@@ -43,8 +43,18 @@ public class FragmentFolders extends FragmentEx {
private Group grpReady;
private FloatingActionButton fab;
private long account;
private AdapterFolder adapter;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Get arguments
Bundle args = getArguments();
account = (args == null ? -1 : args.getLong("account"));
}
@Override
@Nullable
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
@@ -89,10 +99,6 @@ public class FragmentFolders extends FragmentEx {
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
// Get arguments
Bundle args = getArguments();
long account = (args == null ? -1 : args.getLong("account"));
DB db = DB.getInstance(getContext());
// Observe account

View File

@@ -22,11 +22,7 @@ package eu.faircode.email;
import android.content.Context;
import android.content.DialogInterface;
import android.os.Bundle;
import android.text.Editable;
import android.text.Html;
import android.text.TextUtils;
import android.text.TextWatcher;
import android.text.method.LinkMovementMethod;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
@@ -39,7 +35,6 @@ import android.widget.EditText;
import android.widget.ImageButton;
import android.widget.ProgressBar;
import android.widget.Spinner;
import android.widget.TextView;
import android.widget.Toast;
import com.google.android.material.textfield.TextInputLayout;
@@ -60,16 +55,16 @@ import androidx.lifecycle.Observer;
public class FragmentIdentity extends FragmentEx {
private ViewGroup view;
private EditText etName;
private Spinner spAccount;
private Button btnAdvanced;
private EditText etEmail;
private EditText etReplyTo;
private Spinner spProvider;
private Spinner spAccount;
private EditText etHost;
private CheckBox cbStartTls;
private EditText etPort;
private EditText etUser;
private TextInputLayout tilPassword;
private TextView tvLink;
private CheckBox cbSynchronize;
private CheckBox cbPrimary;
private CheckBox cbStoreSent;
@@ -77,7 +72,7 @@ public class FragmentIdentity extends FragmentEx {
private ProgressBar pbSave;
private ImageButton ibDelete;
private ProgressBar pbWait;
private Group grpInstructions;
private Group grpAdvanced;
private long id = -1;
@@ -99,16 +94,16 @@ public class FragmentIdentity extends FragmentEx {
// Get controls
etName = view.findViewById(R.id.etName);
spAccount = view.findViewById(R.id.spAccount);
btnAdvanced = view.findViewById(R.id.btnAdvanced);
etEmail = view.findViewById(R.id.etEmail);
etReplyTo = view.findViewById(R.id.etReplyTo);
spProvider = view.findViewById(R.id.spProvider);
spAccount = view.findViewById(R.id.spAccount);
etHost = view.findViewById(R.id.etHost);
cbStartTls = view.findViewById(R.id.cbStartTls);
etPort = view.findViewById(R.id.etPort);
etUser = view.findViewById(R.id.etUser);
tilPassword = view.findViewById(R.id.tilPassword);
tvLink = view.findViewById(R.id.tvLink);
cbSynchronize = view.findViewById(R.id.cbSynchronize);
cbPrimary = view.findViewById(R.id.cbPrimary);
cbStoreSent = view.findViewById(R.id.cbStoreSent);
@@ -116,28 +111,19 @@ public class FragmentIdentity extends FragmentEx {
pbSave = view.findViewById(R.id.pbSave);
ibDelete = view.findViewById(R.id.ibDelete);
pbWait = view.findViewById(R.id.pbWait);
grpInstructions = view.findViewById(R.id.grpInstructions);
grpAdvanced = view.findViewById(R.id.grpAdvanced);
// Wire controls
etEmail.addTextChangedListener(new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
}
@Override
public void afterTextChanged(Editable s) {
etUser.setText(s.toString());
}
});
spAccount.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
@Override
public void onItemSelected(AdapterView<?> adapterView, View view, int position, long id) {
btnAdvanced.setVisibility(position > 0 ? View.VISIBLE : View.GONE);
if (position == 0)
grpAdvanced.setVisibility(View.GONE);
tilPassword.setPasswordVisibilityToggleEnabled(position == 0);
btnSave.setVisibility(position > 0 ? View.VISIBLE : View.GONE);
Integer tag = (Integer) adapterView.getTag();
if (tag != null && tag.equals(position))
return;
@@ -146,20 +132,36 @@ public class FragmentIdentity extends FragmentEx {
EntityAccount account = (EntityAccount) adapterView.getAdapter().getItem(position);
// Select associated provider
for (int pos = 1; pos < spProvider.getAdapter().getCount(); pos++) {
Provider provider = (Provider) spProvider.getItemAtPosition(pos);
if (provider.imap_host.equals(account.host) && provider.imap_port == account.port) {
spProvider.setSelection(pos);
break;
if (position == 0)
spProvider.setSelection(0);
else {
boolean found = false;
for (int pos = 1; pos < spProvider.getAdapter().getCount(); pos++) {
Provider provider = (Provider) spProvider.getItemAtPosition(pos);
if (provider.imap_host.equals(account.host) &&
provider.imap_port == account.port) {
found = true;
spProvider.setSelection(pos);
// This is needed because the spinner might be invisible
etHost.setText(provider.smtp_host);
etPort.setText(Integer.toString(provider.smtp_port));
cbStartTls.setChecked(provider.starttls);
break;
}
}
if (!found)
grpAdvanced.setVisibility(View.VISIBLE);
}
// Copy account user name
etEmail.setText(account.user);
etUser.setText(account.user);
// Copy account password
tilPassword.getEditText().setText(account.password);
tilPassword.setPasswordVisibilityToggleEnabled(position == 0);
}
@Override
@@ -167,6 +169,14 @@ public class FragmentIdentity extends FragmentEx {
}
});
btnAdvanced.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
int visibility = (grpAdvanced.getVisibility() == View.VISIBLE ? View.GONE : View.VISIBLE);
grpAdvanced.setVisibility(visibility);
}
});
spProvider.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
@Override
public void onItemSelected(AdapterView<?> adapterView, View view, int position, long id) {
@@ -181,10 +191,6 @@ public class FragmentIdentity extends FragmentEx {
etHost.setText(provider.smtp_host);
etPort.setText(position == 0 ? null : Integer.toString(provider.smtp_port));
cbStartTls.setChecked(provider.starttls);
// Show link to instructions
tvLink.setText(Html.fromHtml("<a href=\"" + provider.link + "\">" + provider.link + "</a>"));
grpInstructions.setVisibility(provider.link == null ? View.GONE : View.VISIBLE);
}
@Override
@@ -236,9 +242,9 @@ public class FragmentIdentity extends FragmentEx {
protected Void onLoad(Context context, Bundle args) throws Throwable {
long id = args.getLong("id");
String name = args.getString("name");
long account = args.getLong("account");
String email = args.getString("email");
String replyto = args.getString("replyto");
long account = args.getLong("account");
String host = args.getString("host");
boolean starttls = args.getBoolean("starttls");
String port = args.getString("port");
@@ -253,8 +259,6 @@ public class FragmentIdentity extends FragmentEx {
throw new IllegalArgumentException(getContext().getString(R.string.title_no_name));
if (TextUtils.isEmpty(email))
throw new IllegalArgumentException(getContext().getString(R.string.title_no_email));
if (account < 0)
throw new IllegalArgumentException(getContext().getString(R.string.title_no_account));
if (TextUtils.isEmpty(host))
throw new IllegalArgumentException(getContext().getString(R.string.title_no_host));
if (TextUtils.isEmpty(port))
@@ -267,10 +271,6 @@ public class FragmentIdentity extends FragmentEx {
if (TextUtils.isEmpty(replyto))
replyto = null;
// Refresh token
if (id >= 0 && auth_type == Helper.AUTH_TYPE_GMAIL)
password = Helper.refreshToken(getContext(), "com.google", user, password);
// Check SMTP server
if (synchronize) {
Properties props = MessageHelper.getSessionProperties(context, auth_type);
@@ -293,9 +293,9 @@ public class FragmentIdentity extends FragmentEx {
if (identity == null)
identity = new EntityIdentity();
identity.name = name;
identity.account = account;
identity.email = email;
identity.replyto = replyto;
identity.account = account;
identity.host = host;
identity.port = Integer.parseInt(port);
identity.starttls = starttls;
@@ -388,8 +388,9 @@ public class FragmentIdentity extends FragmentEx {
// Initialize
Helper.setViewsEnabled(view, false);
tilPassword.setPasswordVisibilityToggleEnabled(id < 0);
tvLink.setMovementMethod(LinkMovementMethod.getInstance());
btnSave.setEnabled(false);
btnSave.setVisibility(View.GONE);
btnAdvanced.setVisibility(View.GONE);
grpAdvanced.setVisibility(View.GONE);
pbSave.setVisibility(View.GONE);
ibDelete.setVisibility(View.GONE);
@@ -402,6 +403,7 @@ public class FragmentIdentity extends FragmentEx {
outState.putInt("account", spAccount.getSelectedItemPosition());
outState.putInt("provider", spProvider.getSelectedItemPosition());
outState.putString("password", tilPassword.getEditText().getText().toString());
outState.putInt("advanced", grpAdvanced.getVisibility());
}
@Override
@@ -412,15 +414,15 @@ public class FragmentIdentity extends FragmentEx {
// Observe identity
db.identity().liveIdentity(id).observe(getViewLifecycleOwner(), new Observer<EntityIdentity>() {
boolean once = false;
private boolean once = false;
@Override
public void onChanged(@Nullable final EntityIdentity identity) {
if (savedInstanceState == null) {
if (once)
return;
once = true;
if (once)
return;
once = true;
if (savedInstanceState == null) {
etName.setText(identity == null ? null : identity.name);
etEmail.setText(identity == null ? null : identity.email);
etReplyTo.setText(identity == null ? null : identity.replyto);
@@ -434,23 +436,42 @@ public class FragmentIdentity extends FragmentEx {
cbStoreSent.setChecked(identity == null ? false : identity.store_sent);
etName.requestFocus();
} else
if (identity == null)
new SimpleTask<Integer>() {
@Override
protected Integer onLoad(Context context, Bundle args) {
return DB.getInstance(context).identity().getSynchronizingIdentityCount();
}
@Override
protected void onLoaded(Bundle args, Integer count) {
cbPrimary.setChecked(count == 0);
}
}.load(FragmentIdentity.this, new Bundle());
} else {
tilPassword.getEditText().setText(savedInstanceState.getString("password"));
grpAdvanced.setVisibility(savedInstanceState.getInt("advanced"));
}
Helper.setViewsEnabled(view, true);
grpInstructions.setVisibility(View.GONE);
cbPrimary.setEnabled(cbSynchronize.isChecked());
// Consider previous save/delete as cancelled
ibDelete.setVisibility(identity == null ? View.GONE : View.VISIBLE);
btnSave.setEnabled(true);
pbWait.setVisibility(View.GONE);
db.account().liveAccounts().removeObservers(getViewLifecycleOwner());
db.account().liveAccounts().observe(getViewLifecycleOwner(), new Observer<List<EntityAccount>>() {
private boolean once = false;
@Override
public void onChanged(List<EntityAccount> accounts) {
if (once)
return;
once = true;
if (accounts == null)
accounts = new ArrayList<>();
@@ -503,10 +524,6 @@ public class FragmentIdentity extends FragmentEx {
spAccount.setTag(account);
spAccount.setSelection(account);
}
Provider provider = (Provider) spProvider.getSelectedItem();
tvLink.setText(Html.fromHtml("<a href=\"" + provider.link + "\">" + provider.link + "</a>"));
grpInstructions.setVisibility(provider.link == null ? View.GONE : View.VISIBLE);
}
});
}

View File

@@ -0,0 +1,94 @@
package eu.faircode.email;
/*
This file is part of FairEmail.
FairEmail is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
NetGuard is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with NetGuard. If not, see <http://www.gnu.org/licenses/>.
Copyright 2018 by Marcel Bokhorst (M66B)
*/
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ProgressBar;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.constraintlayout.widget.Group;
import androidx.lifecycle.Observer;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
public class FragmentLogs extends FragmentEx {
private RecyclerView rvLog;
private ProgressBar pbWait;
private Group grpReady;
private AdapterLog adapter;
@Override
@Nullable
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
setSubtitle(R.string.title_log);
View view = inflater.inflate(R.layout.fragment_logs, container, false);
// Get controls
rvLog = view.findViewById(R.id.rvLog);
pbWait = view.findViewById(R.id.pbWait);
grpReady = view.findViewById(R.id.grpReady);
// Wire controls
rvLog.setHasFixedSize(false);
LinearLayoutManager llm = new LinearLayoutManager(getContext());
rvLog.setLayoutManager(llm);
adapter = new AdapterLog(getContext());
rvLog.setAdapter(adapter);
// Initialize
grpReady.setVisibility(View.GONE);
pbWait.setVisibility(View.VISIBLE);
return view;
}
@Override
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
long from = new Date().getTime() - 24 * 3600 * 1000L;
DB db = DB.getInstance(getContext());
db.log().liveLogs(from).observe(getViewLifecycleOwner(), new Observer<List<EntityLog>>() {
@Override
public void onChanged(List<EntityLog> logs) {
if (logs == null)
logs = new ArrayList<>();
adapter.set(logs);
pbWait.setVisibility(View.GONE);
grpReady.setVisibility(View.VISIBLE);
}
});
}
}

View File

@@ -19,7 +19,6 @@ package eu.faircode.email;
Copyright 2018 by Marcel Bokhorst (M66B)
*/
import android.app.PendingIntent;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
@@ -37,8 +36,10 @@ import android.text.Html;
import android.text.Layout;
import android.text.Spannable;
import android.text.Spanned;
import android.text.SpannedString;
import android.text.TextUtils;
import android.text.method.LinkMovementMethod;
import android.text.style.ImageSpan;
import android.text.style.URLSpan;
import android.util.Log;
import android.view.LayoutInflater;
@@ -49,23 +50,16 @@ import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.ProgressBar;
import android.widget.TextView;
import android.widget.Toast;
import com.google.android.material.bottomnavigation.BottomNavigationView;
import com.google.android.material.floatingactionbutton.FloatingActionButton;
import com.google.android.material.snackbar.Snackbar;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.openintents.openpgp.OpenPgpError;
import org.openintents.openpgp.util.OpenPgpApi;
import org.openintents.openpgp.util.OpenPgpServiceConnection;
import org.xml.sax.XMLReader;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
@@ -82,8 +76,6 @@ import java.util.Date;
import java.util.List;
import java.util.Locale;
import javax.mail.internet.InternetAddress;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
@@ -97,13 +89,11 @@ import androidx.localbroadcastmanager.content.LocalBroadcastManager;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import static android.app.Activity.RESULT_OK;
public class FragmentMessage extends FragmentEx {
private ViewGroup view;
private View vwAnswerAnchor;
private ImageView ivFlagged;
private TextView tvFrom;
private TextView tvSize;
private TextView tvTime;
private TextView tvTo;
private TextView tvSubject;
@@ -111,6 +101,8 @@ public class FragmentMessage extends FragmentEx {
private TextView tvReplyTo;
private TextView tvCc;
private TextView tvBcc;
private TextView tvRawHeaders;
private ProgressBar pbRawHeaders;
private RecyclerView rvAttachment;
private TextView tvError;
private View vSeparatorBody;
@@ -121,18 +113,19 @@ public class FragmentMessage extends FragmentEx {
private BottomNavigationView bottom_navigation;
private ProgressBar pbWait;
private Group grpHeader;
private Group grpThread;
private Group grpAddresses;
private Group grpRawHeaders;
private Group grpAttachments;
private Group grpError;
private Group grpMessage;
private TupleMessageEx message = null;
private boolean free = false;
private boolean addresses = false;
private boolean headers = false;
private AdapterAttachment adapter;
private String decrypted = null;
private OpenPgpServiceConnection openPgpConnection = null;
private boolean debug;
private DateFormat df = SimpleDateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT);
@@ -146,18 +139,6 @@ public class FragmentMessage extends FragmentEx {
message = (TupleMessageEx) getArguments().getSerializable("message");
else
message = (TupleMessageEx) savedInstanceState.getSerializable("message");
openPgpConnection = new OpenPgpServiceConnection(getContext(), "org.sufficientlysecure.keychain");
openPgpConnection.bindToService();
}
@Override
public void onDestroy() {
super.onDestroy();
if (openPgpConnection != null) {
openPgpConnection.unbindFromService();
openPgpConnection = null;
}
}
@Override
@@ -170,8 +151,8 @@ public class FragmentMessage extends FragmentEx {
// Get controls
vwAnswerAnchor = view.findViewById(R.id.vwAnswerAnchor);
ivFlagged = view.findViewById(R.id.ivFlagged);
tvFrom = view.findViewById(R.id.tvFrom);
tvSize = view.findViewById(R.id.tvSize);
tvTime = view.findViewById(R.id.tvTime);
tvTo = view.findViewById(R.id.tvTo);
tvSubject = view.findViewById(R.id.tvSubject);
@@ -179,6 +160,8 @@ public class FragmentMessage extends FragmentEx {
tvReplyTo = view.findViewById(R.id.tvReplyTo);
tvCc = view.findViewById(R.id.tvCc);
tvBcc = view.findViewById(R.id.tvBcc);
tvRawHeaders = view.findViewById(R.id.tvRawHeaders);
pbRawHeaders = view.findViewById(R.id.pbRawHeaders);
rvAttachment = view.findViewById(R.id.rvAttachment);
tvError = view.findViewById(R.id.tvError);
vSeparatorBody = view.findViewById(R.id.vSeparatorBody);
@@ -189,24 +172,46 @@ public class FragmentMessage extends FragmentEx {
bottom_navigation = view.findViewById(R.id.bottom_navigation);
pbWait = view.findViewById(R.id.pbWait);
grpHeader = view.findViewById(R.id.grpHeader);
grpThread = view.findViewById(R.id.grpThread);
grpAddresses = view.findViewById(R.id.grpAddresses);
grpRawHeaders = view.findViewById(R.id.grpRawHeaders);
grpAttachments = view.findViewById(R.id.grpAttachments);
grpError = view.findViewById(R.id.grpError);
grpMessage = view.findViewById(R.id.grpMessage);
setHasOptionsMenu(true);
tvCount.setOnClickListener(new View.OnClickListener() {
ivFlagged.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
onMenuThread();
public void onClick(View v) {
Bundle args = new Bundle();
args.putLong("account", message.account);
args.putString("thread", message.thread);
args.putBoolean("flagged", !message.ui_flagged);
Log.i(Helper.TAG, "Set message id=" + message.id + " flagged=" + !message.ui_flagged);
new SimpleTask<Void>() {
@Override
protected Void onLoad(Context context, Bundle args) throws Throwable {
long account = args.getLong("account");
String thread = args.getString("thread");
boolean flagged = args.getBoolean("flagged");
DB db = DB.getInstance(context);
for (EntityMessage message : db.message().getMessageByThread(account, thread)) {
db.message().setMessageUiFlagged(message.id, flagged);
EntityOperation.queue(db, message, EntityOperation.FLAG, flagged);
}
EntityOperation.process(context);
return null;
}
}.load(FragmentMessage.this, args);
}
});
tvBody.setMovementMethod(new LinkMovementMethod() {
public boolean onTouchEvent(TextView widget, Spannable buffer, MotionEvent event) {
if (event.getAction() != MotionEvent.ACTION_UP)
return super.onTouchEvent(widget, buffer, event);
return false;
int x = (int) event.getX();
int y = (int) event.getY();
@@ -239,7 +244,7 @@ public class FragmentMessage extends FragmentEx {
} else if (prefs.getBoolean("webview", false)) {
Bundle args = new Bundle();
args.putString("link", url);
args.putString("url", url);
FragmentWebView fragment = new FragmentWebView();
fragment.setArguments(args);
@@ -273,14 +278,12 @@ public class FragmentMessage extends FragmentEx {
vSeparatorBody.setVisibility(View.GONE);
fab.setVisibility(View.GONE);
tvCount.setVisibility(View.GONE);
grpThread.setVisibility(View.GONE);
grpAddresses.setVisibility(View.GONE);
pbRawHeaders.setVisibility(View.GONE);
grpRawHeaders.setVisibility(View.GONE);
grpAttachments.setVisibility(View.GONE);
grpError.setVisibility(View.GONE);
tvCount.setTag(tvCount.getVisibility());
tvCc.setTag(grpAddresses.getVisibility());
tvError.setTag(grpError.getVisibility());
}
});
@@ -297,10 +300,11 @@ public class FragmentMessage extends FragmentEx {
RecyclerView.Adapter adapter = rvAttachment.getAdapter();
tvCount.setVisibility((int) tvCount.getTag());
grpAddresses.setVisibility((int) tvCc.getTag());
grpThread.setVisibility(View.VISIBLE);
grpAddresses.setVisibility(addresses ? View.VISIBLE : View.GONE);
pbRawHeaders.setVisibility(headers && message.headers == null ? View.VISIBLE : View.GONE);
grpRawHeaders.setVisibility(headers ? View.VISIBLE : View.GONE);
grpAttachments.setVisibility(adapter != null && adapter.getItemCount() > 0 ? View.VISIBLE : View.GONE);
grpError.setVisibility((int) tvError.getTag());
return true;
}
@@ -315,7 +319,7 @@ public class FragmentMessage extends FragmentEx {
case R.id.action_spam:
onActionSpam();
return true;
case R.id.action_trash:
case R.id.action_delete:
onActionDelete();
return true;
case R.id.action_move:
@@ -335,21 +339,22 @@ public class FragmentMessage extends FragmentEx {
// Initialize
grpHeader.setVisibility(View.GONE);
grpAddresses.setVisibility(View.GONE);
pbRawHeaders.setVisibility(View.GONE);
grpRawHeaders.setVisibility(View.GONE);
grpAttachments.setVisibility(View.GONE);
btnImages.setVisibility(View.GONE);
grpMessage.setVisibility(View.GONE);
pbBody.setVisibility(View.GONE);
bottom_navigation.setVisibility(View.GONE);
tvCount.setVisibility(View.GONE);
grpThread.setVisibility(View.GONE);
grpError.setVisibility(View.GONE);
fab.setVisibility(View.GONE);
pbWait.setVisibility(View.VISIBLE);
tvSize.setText(null);
rvAttachment.setHasFixedSize(false);
LinearLayoutManager llm = new LinearLayoutManager(getContext());
rvAttachment.setLayoutManager(llm);
rvAttachment.setItemAnimator(null);
adapter = new AdapterAttachment(getContext(), getViewLifecycleOwner(), true);
rvAttachment.setAdapter(adapter);
@@ -357,17 +362,19 @@ public class FragmentMessage extends FragmentEx {
return view;
}
@Override
public void onDestroyView() {
adapter = null;
super.onDestroyView();
}
@Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putSerializable("message", message);
outState.putBoolean("free", free);
if (free) {
outState.putInt("tag_count", (int) tvCount.getTag());
outState.putInt("tag_cc", (int) tvCc.getTag());
outState.putInt("tag_error", (int) tvError.getTag());
}
outState.putString("decrypted", decrypted);
outState.putBoolean("headers", headers);
outState.putBoolean("addresses", addresses);
}
@Override
@@ -377,6 +384,7 @@ public class FragmentMessage extends FragmentEx {
if (savedInstanceState == null) {
setSubtitle(Helper.localizeFolderName(getContext(), message.folderName));
ivFlagged.setImageResource(message.ui_flagged ? R.drawable.baseline_star_24 : R.drawable.baseline_star_border_24);
tvFrom.setText(MessageHelper.getFormattedAddresses(message.from, true));
tvTime.setText(message.sent == null ? null : df.format(new Date(message.sent)));
tvTo.setText(MessageHelper.getFormattedAddresses(message.to, true));
@@ -388,137 +396,22 @@ public class FragmentMessage extends FragmentEx {
tvCc.setText(MessageHelper.getFormattedAddresses(message.cc, true));
tvBcc.setText(MessageHelper.getFormattedAddresses(message.bcc, true));
tvRawHeaders.setText(message.headers);
tvError.setText(message.error);
} else {
free = savedInstanceState.getBoolean("free");
if (free) {
tvCount.setTag(savedInstanceState.getInt("tag_count"));
tvCc.setTag(savedInstanceState.getInt("tag_cc"));
rvAttachment.setTag(savedInstanceState.getInt("tag_attachment"));
tvError.setTag(savedInstanceState.getInt("tag_error"));
}
decrypted = savedInstanceState.getString("decrypted");
headers = savedInstanceState.getBoolean("headers");
addresses = savedInstanceState.getBoolean("addresses");
}
if (tvBody.getTag() == null) {
// Spanned text needs to be loaded after recreation too
final Bundle args = new Bundle();
args.putLong("id", message.id);
args.putBoolean("has_images", false);
args.putBoolean("show_images", false);
pbBody.setVisibility(View.VISIBLE);
final SimpleTask<Spanned> bodyTask = new SimpleTask<Spanned>() {
@Override
protected Spanned onLoad(final Context context, final Bundle args) throws Throwable {
final long id = args.getLong("id");
final boolean show_images = args.getBoolean("show_images");
String body = (decrypted == null ? message.read(context) : decrypted);
args.putInt("size", body.length());
return Html.fromHtml(HtmlHelper.sanitize(getContext(), body, false), new Html.ImageGetter() {
@Override
public Drawable getDrawable(String source) {
float scale = context.getResources().getDisplayMetrics().density;
int px = (int) (24 * scale + 0.5f);
if (show_images) {
// Get cache folder
File dir = new File(context.getCacheDir(), "images");
dir.mkdir();
// Cleanup cache
long now = new Date().getTime();
File[] images = dir.listFiles();
if (images != null)
for (File image : images)
if (image.isFile() && image.lastModified() + CACHE_IMAGE_DURATION < now) {
Log.i(Helper.TAG, "Deleting from image cache " + image.getName());
image.delete();
}
// Create unique file name
File file = new File(dir, id + "_" + source.hashCode());
InputStream is = null;
FileOutputStream os = null;
try {
// Get input stream
if (file.exists()) {
Log.i(Helper.TAG, "Using cached " + file);
is = new FileInputStream(file);
} else {
Log.i(Helper.TAG, "Downloading " + source);
is = new URL(source).openStream();
}
// Decode image from stream
Bitmap bm = BitmapFactory.decodeStream(is);
if (bm == null)
throw new IllegalArgumentException();
// Cache bitmap
if (!file.exists()) {
os = new FileOutputStream(file);
bm.compress(Bitmap.CompressFormat.PNG, 100, os);
}
// Create drawable from bitmap
Drawable d = new BitmapDrawable(context.getResources(), bm);
d.setBounds(0, 0, bm.getWidth(), bm.getHeight());
return d;
} catch (Throwable ex) {
// Show warning icon
Log.e(Helper.TAG, ex + "\n" + Log.getStackTraceString(ex));
Drawable d = context.getResources().getDrawable(R.drawable.baseline_warning_24, context.getTheme());
d.setBounds(0, 0, px, px);
return d;
} finally {
// Close streams
if (is != null) {
try {
is.close();
} catch (IOException e) {
Log.w(Helper.TAG, e + "\n" + Log.getStackTraceString(e));
}
}
if (os != null) {
try {
os.close();
} catch (IOException e) {
Log.w(Helper.TAG, e + "\n" + Log.getStackTraceString(e));
}
}
}
} else {
// Show placeholder icon
args.putBoolean("has_images", true);
Drawable d = context.getResources().getDrawable(R.drawable.baseline_image_24, context.getTheme());
d.setBounds(0, 0, px, px);
return d;
}
}
}, new Html.TagHandler() {
@Override
public void handleTag(boolean opening, String tag, Editable output, XMLReader xmlReader) {
Log.i(Helper.TAG, "HTML tag=" + tag + " opening=" + opening);
}
});
}
@Override
protected void onLoaded(Bundle args, Spanned body) {
boolean has_images = args.getBoolean("has_images");
boolean show_images = args.getBoolean("show_images");
tvSize.setText(Helper.humanReadableByteCount(args.getInt("size"), false));
tvBody.setText(body);
tvBody.setTag(true);
btnImages.setVisibility(has_images && !show_images ? View.VISIBLE : View.GONE);
grpMessage.setVisibility(View.VISIBLE);
fab.setVisibility(free ? View.GONE : View.VISIBLE);
pbBody.setVisibility(View.GONE);
}
};
bodyTask.load(FragmentMessage.this, args);
@@ -539,89 +432,94 @@ public class FragmentMessage extends FragmentEx {
grpHeader.setVisibility(free ? View.GONE : View.VISIBLE);
vSeparatorBody.setVisibility(free ? View.GONE : View.VISIBLE);
if (free) {
tvCount.setVisibility((int) tvCount.getTag());
grpAddresses.setVisibility((int) tvCc.getTag());
grpError.setVisibility((int) tvError.getTag());
} else {
tvCount.setVisibility(!free && message.count > 1 ? View.VISIBLE : View.GONE);
grpError.setVisibility(free || message.error == null ? View.GONE : View.VISIBLE);
}
grpAddresses.setVisibility(!free && addresses ? View.VISIBLE : View.GONE);
grpThread.setVisibility(free ? View.GONE : View.VISIBLE);
pbRawHeaders.setVisibility(!free && headers && message.headers == null ? View.VISIBLE : View.GONE);
grpRawHeaders.setVisibility(free || !headers ? View.GONE : View.VISIBLE);
grpError.setVisibility(message.error == null ? View.GONE : View.VISIBLE);
DB db = DB.getInstance(getContext());
final DB db = DB.getInstance(getContext());
if (!message.virtual) {
// Observe message
db.message().liveMessage(message.id).observe(getViewLifecycleOwner(), new Observer<TupleMessageEx>() {
// Observe message
db.message().liveMessage(message.id).observe(getViewLifecycleOwner(), new Observer<TupleMessageEx>() {
@Override
public void onChanged(@Nullable final TupleMessageEx message) {
if (message == null || (!(debug && BuildConfig.DEBUG) && message.ui_hide)) {
// Message gone (moved, deleted)
finish();
return;
@Override
public void onChanged(@Nullable final TupleMessageEx message) {
if (message == null || (!(debug && BuildConfig.DEBUG) && message.ui_hide)) {
// Message gone (moved, deleted)
finish();
return;
}
// Messages are immutable except for flags
FragmentMessage.this.message = message;
setSeen();
ivFlagged.setImageResource(message.ui_flagged ? R.drawable.baseline_star_24 : R.drawable.baseline_star_border_24);
// Headers can be downloaded
tvRawHeaders.setText(message.headers);
pbRawHeaders.setVisibility(!free && headers && message.headers == null ? View.VISIBLE : View.GONE);
// Message count can be changed
getActivity().invalidateOptionsMenu();
// Messages can be moved to another folder
setSubtitle(Helper.localizeFolderName(getContext(), message.folderName));
// Observe folders
db.folder().liveFolders(message.account).removeObservers(getViewLifecycleOwner());
db.folder().liveFolders(message.account).observe(getViewLifecycleOwner(), new Observer<List<TupleFolderEx>>() {
@Override
public void onChanged(@Nullable List<TupleFolderEx> folders) {
boolean hasTrash = false;
boolean hasJunk = false;
boolean hasArchive = false;
boolean hasUser = false;
if (folders != null)
for (EntityFolder folder : folders) {
if (EntityFolder.TRASH.equals(folder.type))
hasTrash = true;
else if (EntityFolder.JUNK.equals(folder.type))
hasJunk = true;
else if (EntityFolder.ARCHIVE.equals(folder.type))
hasArchive = true;
else if (EntityFolder.USER.equals(folder.type))
hasUser = true;
}
boolean inInbox = EntityFolder.INBOX.equals(message.folderType);
boolean inOutbox = EntityFolder.OUTBOX.equals(message.folderType);
boolean inArchive = EntityFolder.ARCHIVE.equals(message.folderType);
boolean inTrash = EntityFolder.TRASH.equals(message.folderType);
boolean inJunk = EntityFolder.JUNK.equals(message.folderType);
bottom_navigation.setTag(inTrash || !hasTrash || inOutbox);
bottom_navigation.getMenu().findItem(R.id.action_spam).setVisible(message.uid != null && !inArchive && !inJunk && hasJunk);
bottom_navigation.getMenu().findItem(R.id.action_delete).setVisible((message.uid != null && hasTrash) || (inOutbox && !TextUtils.isEmpty(message.error)));
bottom_navigation.getMenu().findItem(R.id.action_move).setVisible(message.uid != null && (!inInbox || hasUser));
bottom_navigation.getMenu().findItem(R.id.action_archive).setVisible(message.uid != null && !inArchive && hasArchive);
bottom_navigation.getMenu().findItem(R.id.action_reply).setVisible(!inOutbox);
bottom_navigation.setVisibility(View.VISIBLE);
}
});
}
});
// Messages are immutable except for flags
FragmentMessage.this.message.seen = message.seen;
FragmentMessage.this.message.ui_seen = message.ui_seen;
setSeen();
}
});
// Observe attachments
db.attachment().liveAttachments(message.id).observe(getViewLifecycleOwner(),
new Observer<List<EntityAttachment>>() {
@Override
public void onChanged(@Nullable List<EntityAttachment> attachments) {
if (attachments == null)
attachments = new ArrayList<>();
// Observe attachments
db.attachment().liveAttachments(message.id).observe(getViewLifecycleOwner(),
new Observer<List<EntityAttachment>>() {
@Override
public void onChanged(@Nullable List<EntityAttachment> attachments) {
if (attachments == null)
attachments = new ArrayList<>();
adapter.set(attachments);
grpAttachments.setVisibility(!free && attachments.size() > 0 ? View.VISIBLE : View.GONE);
}
});
adapter.set(attachments);
grpAttachments.setVisibility(!free && attachments.size() > 0 ? View.VISIBLE : View.GONE);
}
});
// Observe folders
db.folder().liveFolders(message.account).observe(getViewLifecycleOwner(), new Observer<List<TupleFolderEx>>() {
@Override
public void onChanged(@Nullable List<TupleFolderEx> folders) {
if (folders == null)
folders = new ArrayList<>();
boolean inInbox = EntityFolder.INBOX.equals(message.folderType);
boolean inOutbox = EntityFolder.OUTBOX.equals(message.folderType);
boolean inArchive = EntityFolder.ARCHIVE.equals(message.folderType);
boolean inTrash = EntityFolder.TRASH.equals(message.folderType);
boolean inJunk = EntityFolder.JUNK.equals(message.folderType);
boolean hasTrash = false;
boolean hasJunk = false;
boolean hasArchive = false;
boolean hasUser = false;
if (folders != null)
for (EntityFolder folder : folders) {
if (EntityFolder.TRASH.equals(folder.type))
hasTrash = true;
else if (EntityFolder.JUNK.equals(folder.type))
hasJunk = true;
else if (EntityFolder.ARCHIVE.equals(folder.type))
hasArchive = true;
else if (EntityFolder.USER.equals(folder.type))
hasUser = true;
}
bottom_navigation.setTag(inTrash || !hasTrash || inOutbox);
bottom_navigation.getMenu().findItem(R.id.action_spam).setVisible(message.uid != null && !inArchive && !inJunk && hasJunk);
bottom_navigation.getMenu().findItem(R.id.action_trash).setVisible((message.uid != null && hasTrash) || (inOutbox && !TextUtils.isEmpty(message.error)));
bottom_navigation.getMenu().findItem(R.id.action_move).setVisible(message.uid != null && (!inInbox || hasUser));
bottom_navigation.getMenu().findItem(R.id.action_archive).setVisible(message.uid != null && !inArchive && hasArchive);
bottom_navigation.getMenu().findItem(R.id.action_reply).setVisible(!inOutbox);
bottom_navigation.setVisibility(View.VISIBLE);
}
});
}
}
private void setSeen() {
@@ -650,17 +548,12 @@ public class FragmentMessage extends FragmentEx {
boolean inOutbox = EntityFolder.OUTBOX.equals(message.folderType);
menu.findItem(R.id.menu_addresses).setVisible(!free);
menu.findItem(R.id.menu_thread).setVisible(!free && !message.virtual && message.count > 1);
menu.findItem(R.id.menu_seen).setVisible(!free && !message.virtual && !inOutbox);
menu.findItem(R.id.menu_forward).setVisible(!free && !message.virtual && !inOutbox);
menu.findItem(R.id.menu_reply_all).setVisible(!free && !message.virtual && message.cc != null && !inOutbox);
menu.findItem(R.id.menu_decrypt).setVisible(decrypted == null);
MenuItem menuSeen = menu.findItem(R.id.menu_seen);
menuSeen.setIcon(message.ui_seen
? R.drawable.baseline_visibility_off_24
: R.drawable.baseline_visibility_24);
menuSeen.setTitle(message.ui_seen ? R.string.title_unseen : R.string.title_seen);
menu.findItem(R.id.menu_thread).setVisible(message.count > 1);
menu.findItem(R.id.menu_forward).setVisible(!inOutbox);
menu.findItem(R.id.menu_show_headers).setChecked(headers);
menu.findItem(R.id.menu_show_headers).setEnabled(message.uid != null);
menu.findItem(R.id.menu_show_headers).setVisible(!free);
menu.findItem(R.id.menu_reply_all).setVisible(!inOutbox);
}
@Override
@@ -672,28 +565,29 @@ public class FragmentMessage extends FragmentEx {
case R.id.menu_thread:
onMenuThread();
return true;
case R.id.menu_seen:
onMenuSeen();
return true;
case R.id.menu_forward:
onMenuForward();
return true;
case R.id.menu_reply_all:
onMenuReplyAll();
return true;
case R.id.menu_show_html:
onMenuShowHtml();
return true;
case R.id.menu_show_headers:
onMenuShowHeaders();
return true;
case R.id.menu_answer:
onMenuAnswer();
return true;
case R.id.menu_decrypt:
onMenuDecrypt();
return true;
default:
return super.onOptionsItemSelected(item);
}
}
private void onMenuAddresses() {
grpAddresses.setVisibility(grpAddresses.getVisibility() == View.GONE ? View.VISIBLE : View.GONE);
addresses = !addresses;
grpAddresses.setVisibility(addresses ? View.VISIBLE : View.GONE);
}
private void onMenuThread() {
@@ -710,49 +604,6 @@ public class FragmentMessage extends FragmentEx {
fragmentTransaction.commit();
}
private void onMenuSeen() {
Helper.setViewsEnabled(view, false);
Bundle args = new Bundle();
args.putLong("id", message.id);
new SimpleTask<Void>() {
@Override
protected Void onLoad(Context context, Bundle args) {
long id = args.getLong("id");
DB db = DB.getInstance(context);
try {
db.beginTransaction();
EntityMessage message = db.message().getMessage(id);
for (EntityMessage tmessage : db.message().getMessageByThread(message.account, message.thread)) {
db.message().setMessageUiSeen(tmessage.id, !message.ui_seen);
EntityOperation.queue(db, tmessage, EntityOperation.SEEN, !tmessage.ui_seen);
}
db.setTransactionSuccessful();
} finally {
db.endTransaction();
}
EntityOperation.process(context);
return null;
}
@Override
protected void onLoaded(Bundle args, Void data) {
Helper.setViewsEnabled(view, true);
}
@Override
public void onException(Bundle args, Throwable ex) {
Helper.setViewsEnabled(view, true);
Toast.makeText(getContext(), ex.toString(), Toast.LENGTH_LONG).show();
}
}.load(this, args);
}
private void onMenuForward() {
startActivity(new Intent(getContext(), ActivityCompose.class)
.putExtra("action", "forward")
@@ -765,6 +616,53 @@ public class FragmentMessage extends FragmentEx {
.putExtra("reference", message.id));
}
private void onMenuShowHeaders() {
headers = !headers;
getActivity().invalidateOptionsMenu();
pbRawHeaders.setVisibility(headers && message.headers == null ? View.VISIBLE : View.GONE);
grpRawHeaders.setVisibility(headers ? View.VISIBLE : View.GONE);
if (headers && message.headers == null) {
Bundle args = new Bundle();
args.putLong("id", message.id);
new SimpleTask<Void>() {
@Override
protected Void onLoad(Context context, Bundle args) {
Long id = args.getLong("id");
DB db = DB.getInstance(context);
EntityMessage message = db.message().getMessage(id);
EntityOperation.queue(db, message, EntityOperation.HEADERS);
EntityOperation.process(context);
return null;
}
}.load(this, args);
}
}
private void onMenuShowHtml() {
new SimpleTask<String>() {
@Override
protected String onLoad(Context context, Bundle args) throws Throwable {
return message.read(context);
}
@Override
protected void onLoaded(Bundle a, String html) {
Bundle args = new Bundle();
args.putString("html", html);
args.putString("from", MessageHelper.getFormattedAddresses(message.from, true));
FragmentWebView fragment = new FragmentWebView();
fragment.setArguments(args);
FragmentTransaction fragmentTransaction = getFragmentManager().beginTransaction();
fragmentTransaction.replace(R.id.content_frame, fragment).addToBackStack("webview");
fragmentTransaction.commit();
}
}.load(this, new Bundle());
}
private void onMenuAnswer() {
DB.getInstance(getContext()).answer().liveAnswers().observe(getViewLifecycleOwner(), new Observer<List<EntityAnswer>>() {
@Override
@@ -808,100 +706,6 @@ public class FragmentMessage extends FragmentEx {
});
}
private void onMenuDecrypt() {
Log.i(Helper.TAG, "On decrypt");
if (!PreferenceManager.getDefaultSharedPreferences(getContext()).getBoolean("pro", false)) {
FragmentTransaction fragmentTransaction = getFragmentManager().beginTransaction();
fragmentTransaction.replace(R.id.content_frame, new FragmentPro()).addToBackStack("pro");
fragmentTransaction.commit();
return;
}
try {
if (openPgpConnection == null || !openPgpConnection.isBound())
throw new IllegalArgumentException(getString(R.string.title_no_openpgp));
if (message.to == null || message.to.length == 0)
throw new IllegalArgumentException(getString(R.string.title_to_missing));
// Find encrypted message
String begin = "-----BEGIN PGP MESSAGE-----";
String end = "-----END PGP MESSAGE-----";
Document document = Jsoup.parse(message.read(getContext()));
String encrypted = document.text();
int efrom = encrypted.indexOf(begin) + begin.length();
int eto = encrypted.indexOf(end);
if (efrom < 0 || eto < 0)
throw new IllegalArgumentException(getString(R.string.title_not_encrypted));
encrypted = begin + "\n" + encrypted.substring(efrom, eto).replace(" ", "\n") + end + "\n";
final InputStream is = new ByteArrayInputStream(encrypted.getBytes("UTF-8"));
final ByteArrayOutputStream os = new ByteArrayOutputStream();
InternetAddress to = (InternetAddress) message.to[0];
Intent data = new Intent();
data.setAction(OpenPgpApi.ACTION_DECRYPT_VERIFY);
data.putExtra(OpenPgpApi.EXTRA_USER_IDS, new String[]{to.getAddress()});
OpenPgpApi api = new OpenPgpApi(getContext(), openPgpConnection.getService());
api.executeApiAsync(data, is, os, new OpenPgpApi.IOpenPgpCallback() {
@Override
public void onReturn(Intent result) {
try {
int code = result.getIntExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_ERROR);
switch (code) {
case OpenPgpApi.RESULT_CODE_SUCCESS: {
Log.i(Helper.TAG, "Decrypted");
FragmentMessage.this.decrypted = os.toString("UTF-8");
getActivity().invalidateOptionsMenu();
tvBody.setText(decrypted);
break;
}
case OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED: {
Log.i(Helper.TAG, "User interaction");
PendingIntent pi = result.getParcelableExtra(OpenPgpApi.RESULT_INTENT);
startIntentSenderForResult(
pi.getIntentSender(),
ActivityView.REQUEST_OPENPGP,
null, 0, 0, 0,
new Bundle());
break;
}
case OpenPgpApi.RESULT_CODE_ERROR: {
OpenPgpError error = result.getParcelableExtra(OpenPgpApi.RESULT_ERROR);
throw new IllegalArgumentException(error.getMessage());
}
}
} catch (Throwable ex) {
Log.e(Helper.TAG, ex + "\n" + Log.getStackTraceString(ex));
if (ex instanceof IllegalArgumentException)
Snackbar.make(view, ex.getMessage(), Snackbar.LENGTH_LONG).show();
else
Toast.makeText(getContext(), ex.toString(), Toast.LENGTH_LONG).show();
}
}
});
} catch (Throwable ex) {
Log.e(Helper.TAG, ex + "\n" + Log.getStackTraceString(ex));
if (ex instanceof IllegalArgumentException)
Snackbar.make(view, ex.getMessage(), Snackbar.LENGTH_LONG).show();
else
Toast.makeText(getContext(), ex.toString(), Toast.LENGTH_LONG).show();
}
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
Log.i(Helper.TAG, "Message onActivityResult request=" + requestCode + " result=" + resultCode + " data=" + data);
if (resultCode == RESULT_OK) {
if (requestCode == ActivityView.REQUEST_OPENPGP) {
Log.i(Helper.TAG, "User interacted");
onMenuDecrypt();
}
}
}
private void onActionSpam() {
AlertDialog.Builder builder = new AlertDialog.Builder(getContext());
builder
@@ -1221,4 +1025,123 @@ public class FragmentMessage extends FragmentEx {
.putExtra("action", "reply")
.putExtra("reference", message.id));
}
private SimpleTask<Spanned> bodyTask = new SimpleTask<Spanned>() {
@Override
protected Spanned onLoad(final Context context, final Bundle args) throws Throwable {
final long id = args.getLong("id");
final boolean show_images = args.getBoolean("show_images");
String body = message.read(context);
args.putInt("size", body.length());
return decodeHtml(context, id, body, show_images);
}
@Override
protected void onLoaded(Bundle args, Spanned body) {
boolean show_images = args.getBoolean("show_images");
SpannedString ss = new SpannedString(body);
boolean has_images = (ss.getSpans(0, ss.length(), ImageSpan.class).length > 0);
tvBody.setText(body);
tvBody.setTag(true);
btnImages.setVisibility(has_images && !show_images ? View.VISIBLE : View.GONE);
grpMessage.setVisibility(View.VISIBLE);
fab.setVisibility(free ? View.GONE : View.VISIBLE);
pbBody.setVisibility(View.GONE);
}
};
private static Spanned decodeHtml(final Context context, final long id, String body, final boolean show_images) {
return Html.fromHtml(HtmlHelper.sanitize(context, body, false), new Html.ImageGetter() {
@Override
public Drawable getDrawable(String source) {
float scale = context.getResources().getDisplayMetrics().density;
int px = (int) (24 * scale + 0.5f);
if (show_images) {
// Get cache folder
File dir = new File(context.getCacheDir(), "images");
dir.mkdir();
// Cleanup cache
long now = new Date().getTime();
File[] images = dir.listFiles();
if (images != null)
for (File image : images)
if (image.isFile() && image.lastModified() + CACHE_IMAGE_DURATION < now) {
Log.i(Helper.TAG, "Deleting from image cache " + image.getName());
image.delete();
}
InputStream is = null;
FileOutputStream os = null;
try {
if (source == null)
throw new IllegalArgumentException("Html.ImageGetter.getDrawable(source == null)");
// Create unique file name
File file = new File(dir, id + "_" + source.hashCode());
// Get input stream
if (file.exists()) {
Log.i(Helper.TAG, "Using cached " + file);
is = new FileInputStream(file);
} else {
Log.i(Helper.TAG, "Downloading " + source);
is = new URL(source).openStream();
}
// Decode image from stream
Bitmap bm = BitmapFactory.decodeStream(is);
if (bm == null)
throw new IllegalArgumentException();
// Cache bitmap
if (!file.exists()) {
os = new FileOutputStream(file);
bm.compress(Bitmap.CompressFormat.PNG, 100, os);
}
// Create drawable from bitmap
Drawable d = new BitmapDrawable(context.getResources(), bm);
d.setBounds(0, 0, bm.getWidth(), bm.getHeight());
return d;
} catch (Throwable ex) {
// Show warning icon
Log.e(Helper.TAG, ex + "\n" + Log.getStackTraceString(ex));
Drawable d = context.getResources().getDrawable(R.drawable.baseline_warning_24, context.getTheme());
d.setBounds(0, 0, px, px);
return d;
} finally {
// Close streams
if (is != null) {
try {
is.close();
} catch (IOException e) {
Log.w(Helper.TAG, e + "\n" + Log.getStackTraceString(e));
}
}
if (os != null) {
try {
os.close();
} catch (IOException e) {
Log.w(Helper.TAG, e + "\n" + Log.getStackTraceString(e));
}
}
}
} else {
// Show placeholder icon
Drawable d = context.getResources().getDrawable(R.drawable.baseline_image_24, context.getTheme());
d.setBounds(0, 0, px, px);
return d;
}
}
}, new Html.TagHandler() {
@Override
public void handleTag(boolean opening, String tag, Editable output, XMLReader xmlReader) {
Log.i(Helper.TAG, "HTML tag=" + tag + " opening=" + opening);
}
});
}
}

View File

@@ -22,6 +22,8 @@ package eu.faircode.email;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.graphics.Canvas;
import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.preference.PreferenceManager;
import android.text.TextUtils;
@@ -40,6 +42,7 @@ import android.widget.Toast;
import com.google.android.material.floatingactionbutton.FloatingActionButton;
import com.google.android.material.snackbar.Snackbar;
import java.util.Date;
import java.util.List;
import androidx.annotation.NonNull;
@@ -49,7 +52,6 @@ import androidx.constraintlayout.widget.Group;
import androidx.fragment.app.FragmentTransaction;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.Observer;
import androidx.paging.DataSource;
import androidx.paging.LivePagedListBuilder;
import androidx.paging.PagedList;
import androidx.recyclerview.widget.ItemTouchHelper;
@@ -58,7 +60,7 @@ import androidx.recyclerview.widget.RecyclerView;
public class FragmentMessages extends FragmentEx {
private ViewGroup view;
private Button btnHintSwipe;
private Button btnHintActions;
private RecyclerView rvMessage;
private TextView tvNoEmail;
private ProgressBar pbWait;
@@ -70,14 +72,17 @@ public class FragmentMessages extends FragmentEx {
private long thread = -1;
private String search = null;
private SearchDataSource sds = null;
private long primary = -1;
private AdapterMessage adapter;
private SearchState searchState = SearchState.Reset;
private BoundaryCallbackMessages searchCallback = null;
private static final int MESSAGES_PAGE_SIZE = 50;
private static final int SEARCH_PAGE_SIZE = 10;
private enum SearchState {Reset, Database, Boundary}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
@@ -99,7 +104,7 @@ public class FragmentMessages extends FragmentEx {
setHasOptionsMenu(true);
// Get controls
btnHintSwipe = view.findViewById(R.id.btnHintSwipe);
btnHintActions = view.findViewById(R.id.btnHintActions);
rvMessage = view.findViewById(R.id.rvFolder);
tvNoEmail = view.findViewById(R.id.tvNoEmail);
pbWait = view.findViewById(R.id.pbWait);
@@ -110,10 +115,10 @@ public class FragmentMessages extends FragmentEx {
// Wire controls
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getContext());
btnHintSwipe.setOnClickListener(new View.OnClickListener() {
btnHintActions.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
prefs.edit().putBoolean("understood_swipe", true).apply();
prefs.edit().putBoolean("understood_actions", true).apply();
grpHintSwipe.setVisibility(View.GONE);
}
});
@@ -156,65 +161,101 @@ public class FragmentMessages extends FragmentEx {
return false;
}
@Override
public void onChildDraw(Canvas canvas, RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, float dX, float dY, int actionState, boolean isCurrentlyActive) {
int pos = viewHolder.getAdapterPosition();
if (pos == RecyclerView.NO_POSITION)
return;
TupleMessageEx message = ((AdapterMessage) rvMessage.getAdapter()).getCurrentList().get(pos);
boolean inbox = (EntityFolder.ARCHIVE.equals(message.folderType) || EntityFolder.TRASH.equals(message.folderType));
View itemView = viewHolder.itemView;
int margin = Math.round(12 * (getResources().getDisplayMetrics().density));
if (dX > margin) {
// Right swipe
Drawable d = getResources().getDrawable(inbox ? R.drawable.baseline_inbox_24 : R.drawable.baseline_archive_24, getContext().getTheme());
d.setBounds(
itemView.getLeft() + margin,
itemView.getTop() + d.getIntrinsicHeight() / 2,
itemView.getLeft() + margin + d.getIntrinsicWidth(),
itemView.getTop() + (itemView.getHeight() - d.getIntrinsicHeight() / 2));
d.draw(canvas);
} else if (dX < -margin) {
// Left swipe
Drawable d = getResources().getDrawable(inbox ? R.drawable.baseline_inbox_24 : R.drawable.baseline_delete_24, getContext().getTheme());
d.setBounds(
itemView.getLeft() + itemView.getWidth() - d.getIntrinsicWidth() - margin,
itemView.getTop() + d.getIntrinsicHeight() / 2,
itemView.getLeft() + itemView.getWidth() - margin,
itemView.getTop() + (itemView.getHeight() - d.getIntrinsicHeight() / 2));
d.draw(canvas);
}
super.onChildDraw(canvas, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive);
}
@Override
public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) {
int pos = viewHolder.getAdapterPosition();
if (pos != RecyclerView.NO_POSITION) {
TupleMessageEx message = ((AdapterMessage) rvMessage.getAdapter()).getCurrentList().get(pos);
Log.i(Helper.TAG, "Swiped dir=" + direction + " message=" + message.id);
if (pos == RecyclerView.NO_POSITION)
return;
Bundle args = new Bundle();
args.putLong("id", message.id);
args.putInt("direction", direction);
new SimpleTask<String>() {
@Override
protected String onLoad(Context context, Bundle args) throws Throwable {
long id = args.getLong("id");
int direction = args.getInt("direction");
EntityFolder target = null;
TupleMessageEx message = ((AdapterMessage) rvMessage.getAdapter()).getCurrentList().get(pos);
Log.i(Helper.TAG, "Swiped dir=" + direction + " message=" + message.id);
DB db = DB.getInstance(context);
try {
db.beginTransaction();
EntityMessage message = db.message().getMessage(id);
EntityFolder folder = db.folder().getFolder(message.folder);
Bundle args = new Bundle();
args.putLong("id", message.id);
args.putInt("direction", direction);
new SimpleTask<String>() {
@Override
protected String onLoad(Context context, Bundle args) {
long id = args.getLong("id");
int direction = args.getInt("direction");
EntityFolder target = null;
if (EntityFolder.ARCHIVE.equals(folder.type) || EntityFolder.TRASH.equals(folder.type))
target = db.folder().getFolderByType(message.account, EntityFolder.INBOX);
else {
if (direction == ItemTouchHelper.RIGHT)
target = db.folder().getFolderByType(message.account, EntityFolder.ARCHIVE);
if (direction == ItemTouchHelper.LEFT || target == null)
target = db.folder().getFolderByType(message.account, EntityFolder.TRASH);
}
DB db = DB.getInstance(context);
try {
db.beginTransaction();
EntityMessage message = db.message().getMessage(id);
EntityFolder folder = db.folder().getFolder(message.folder);
db.message().setMessageUiHide(message.id, true);
EntityOperation.queue(db, message, EntityOperation.MOVE, target.id);
db.setTransactionSuccessful();
} finally {
db.endTransaction();
if (EntityFolder.ARCHIVE.equals(folder.type) || EntityFolder.TRASH.equals(folder.type))
target = db.folder().getFolderByType(message.account, EntityFolder.INBOX);
else {
if (direction == ItemTouchHelper.RIGHT)
target = db.folder().getFolderByType(message.account, EntityFolder.ARCHIVE);
if (direction == ItemTouchHelper.LEFT || target == null)
target = db.folder().getFolderByType(message.account, EntityFolder.TRASH);
}
EntityOperation.process(context);
db.message().setMessageUiHide(message.id, true);
EntityOperation.queue(db, message, EntityOperation.MOVE, target.id);
return target.name;
db.setTransactionSuccessful();
} finally {
db.endTransaction();
}
@Override
protected void onLoaded(Bundle args, String folder) {
Snackbar.make(
view,
getString(R.string.title_moving, Helper.localizeFolderName(getContext(), folder)),
Snackbar.LENGTH_SHORT).show();
}
EntityOperation.process(context);
@Override
protected void onException(Bundle args, Throwable ex) {
Toast.makeText(getContext(), ex.toString(), Toast.LENGTH_LONG).show();
}
}.load(FragmentMessages.this, args);
}
return target.name;
}
@Override
protected void onLoaded(Bundle args, String folder) {
Snackbar.make(
view,
getString(R.string.title_moving, Helper.localizeFolderName(getContext(), folder)),
Snackbar.LENGTH_SHORT).show();
}
@Override
protected void onException(Bundle args, Throwable ex) {
Toast.makeText(getContext(), ex.toString(), Toast.LENGTH_LONG).show();
}
}.load(FragmentMessages.this, args);
}
}).attachToRecyclerView(rvMessage);
@@ -243,7 +284,7 @@ public class FragmentMessages extends FragmentEx {
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getContext());
grpHintSwipe.setVisibility(prefs.getBoolean("understood_swipe", false) ? View.GONE : View.VISIBLE);
grpHintSwipe.setVisibility(prefs.getBoolean("understood_actions", false) ? View.GONE : View.VISIBLE);
final DB db = DB.getInstance(getContext());
@@ -294,61 +335,131 @@ public class FragmentMessages extends FragmentEx {
}
});
messages = new LivePagedListBuilder<>(db.message().pagedFolder(folder, debug), MESSAGES_PAGE_SIZE).build();
messages = new LivePagedListBuilder<>(db.message().pagedFolder(folder, false, debug), MESSAGES_PAGE_SIZE).build();
}
else {
setSubtitle(R.string.title_folder_thread);
messages = new LivePagedListBuilder<>(db.message().pagedThread(thread, debug), MESSAGES_PAGE_SIZE).build();
}
messages.observe(getViewLifecycleOwner(), new Observer<PagedList<TupleMessageEx>>() {
@Override
public void onChanged(@Nullable PagedList<TupleMessageEx> messages) {
if (messages == null) {
finish();
return;
}
Log.i(Helper.TAG, "Submit messages=" + messages.size());
adapter.submitList(messages);
pbWait.setVisibility(View.GONE);
grpReady.setVisibility(View.VISIBLE);
if (messages.size() == 0) {
tvNoEmail.setVisibility(View.VISIBLE);
rvMessage.setVisibility(View.GONE);
} else {
tvNoEmail.setVisibility(View.GONE);
rvMessage.setVisibility(View.VISIBLE);
}
}
});
} else {
Log.i(Helper.TAG, "Search state=" + searchState);
setSubtitle(getString(R.string.title_searching, search));
// Searching is expensive:
// - reuse existing data source
// - use fragment lifecycle (instead of getViewLifecycleOwner)
// - saving state is not feasible
if (sds == null)
sds = new SearchDataSource(getContext(), this, folder, search);
if (searchCallback == null)
searchCallback = new BoundaryCallbackMessages(
getContext(), FragmentMessages.this,
folder, search,
new BoundaryCallbackMessages.IBoundaryCallbackMessages() {
@Override
public void onLoading() {
pbWait.setVisibility(View.VISIBLE);
}
messages = new LivePagedListBuilder<>(
new DataSource.Factory<Integer, TupleMessageEx>() {
@Override
public void onLoaded() {
pbWait.setVisibility(View.GONE);
}
@Override
public void onError(Context context, Throwable ex) {
Toast.makeText(context, ex.toString(), Toast.LENGTH_LONG).show();
}
});
Bundle args = new Bundle();
args.putLong("folder", folder);
args.putString("search", search);
new SimpleTask<Void>() {
@Override
protected Void onLoad(Context context, Bundle args) {
if (searchState == SearchState.Reset) {
long folder = args.getLong("folder");
DB.getInstance(context).message().resetFound(folder);
searchState = SearchState.Database;
Log.i(Helper.TAG, "Search reset done");
}
return null;
}
@Override
protected void onLoaded(final Bundle args, Void data) {
LivePagedListBuilder<Integer, TupleMessageEx> builder = new LivePagedListBuilder<>(db.message().pagedFolder(folder, true, false), SEARCH_PAGE_SIZE);
builder.setBoundaryCallback(searchCallback);
LiveData<PagedList<TupleMessageEx>> messages = builder.build();
messages.observe(getViewLifecycleOwner(), new Observer<PagedList<TupleMessageEx>>() {
@Override
public DataSource<Integer, TupleMessageEx> create() {
return sds;
public void onChanged(PagedList<TupleMessageEx> messages) {
Log.i(Helper.TAG, "Submit found messages=" + messages.size());
adapter.submitList(messages);
grpReady.setVisibility(View.VISIBLE);
}
},
new PagedList.Config.Builder()
.setEnablePlaceholders(true)
.setInitialLoadSizeHint(SEARCH_PAGE_SIZE)
.setPageSize(SEARCH_PAGE_SIZE)
.build()
).build();
});
new SimpleTask<Long>() {
@Override
protected Long onLoad(Context context, Bundle args) throws Throwable {
long last = 0;
if (searchState == SearchState.Database) {
last = new Date().getTime();
long folder = args.getLong("folder");
String search = args.getString("search").toLowerCase();
DB db = DB.getInstance(context);
for (long id : db.message().getMessageIDs(folder)) {
EntityMessage message = db.message().getMessage(id);
if (message != null) { // Message could be removed in the meantime
String from = MessageHelper.getFormattedAddresses(message.from, true);
if (from.toLowerCase().contains(search) ||
message.subject.toLowerCase().contains(search) ||
message.read(context).toLowerCase().contains(search)) {
Log.i(Helper.TAG, "Search found id=" + id);
db.message().setMessageFound(message.id, true);
last = message.received;
}
}
}
searchState = SearchState.Boundary;
Log.i(Helper.TAG, "Search database done");
}
return last;
}
@Override
protected void onLoaded(Bundle args, Long last) {
pbWait.setVisibility(View.GONE);
searchCallback.setEnabled(true);
if (last > 0)
searchCallback.load(last);
}
}.load(FragmentMessages.this, args);
}
}.load(this, args);
}
messages.observe(getViewLifecycleOwner(), new Observer<PagedList<TupleMessageEx>>() {
@Override
public void onChanged(@Nullable PagedList<TupleMessageEx> messages) {
if (messages == null) {
finish();
return;
}
Log.i(Helper.TAG, "Submit messages=" + messages.size());
adapter.submitList(messages);
pbWait.setVisibility(View.GONE);
grpReady.setVisibility(View.VISIBLE);
if (messages.size() == 0) {
tvNoEmail.setVisibility(View.VISIBLE);
rvMessage.setVisibility(View.GONE);
} else {
tvNoEmail.setVisibility(View.GONE);
rvMessage.setVisibility(View.VISIBLE);
}
}
});
Bundle args = new Bundle();
args.putLong("folder", folder);
args.putLong("thread", thread);

View File

@@ -20,45 +20,60 @@ package eu.faircode.email;
*/
import android.Manifest;
import android.annotation.TargetApi;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.graphics.drawable.Drawable;
import android.net.ConnectivityManager;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.PowerManager;
import android.preference.PreferenceManager;
import android.provider.Settings;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.CheckBox;
import android.widget.CompoundButton;
import android.widget.ProgressBar;
import android.widget.TextView;
import android.widget.Toast;
import android.widget.ToggleButton;
import java.util.List;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.core.content.ContextCompat;
import androidx.fragment.app.FragmentTransaction;
import androidx.lifecycle.Observer;
public class FragmentSetup extends FragmentEx {
private Button btnAccount;
private ProgressBar pbAccount;
private TextView tvAccountDone;
private Button btnIdentity;
private ProgressBar pbIdentity;
private TextView tvIdentityDone;
private Button btnPermissions;
private TextView tvPermissionsDone;
private CheckBox cbDarkTheme;
private Button btnDoze;
private TextView tvDozeDone;
private Button btnData;
private ToggleButton tbDarkTheme;
private Button btnOptions;
private Drawable check;
private static final String[] permissions = new String[]{
Manifest.permission.READ_CONTACTS
};
@@ -68,21 +83,26 @@ public class FragmentSetup extends FragmentEx {
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
setSubtitle(R.string.title_setup);
check = getResources().getDrawable(R.drawable.baseline_check_24, getContext().getTheme());
View view = inflater.inflate(R.layout.fragment_setup, container, false);
// Get controls
btnAccount = view.findViewById(R.id.btnAccount);
pbAccount = view.findViewById(R.id.pbAccount);
tvAccountDone = view.findViewById(R.id.tvAccountDone);
btnIdentity = view.findViewById(R.id.btnIdentity);
pbIdentity = view.findViewById(R.id.pbIdentity);
tvIdentityDone = view.findViewById(R.id.tvIdentityDone);
btnPermissions = view.findViewById(R.id.btnPermissions);
tvPermissionsDone = view.findViewById(R.id.tvPermissionsDone);
cbDarkTheme = view.findViewById(R.id.cbDarkTheme);
btnDoze = view.findViewById(R.id.btnDoze);
tvDozeDone = view.findViewById(R.id.tvDozeDone);
btnData = view.findViewById(R.id.btnData);
tbDarkTheme = view.findViewById(R.id.tbDarkTheme);
btnOptions = view.findViewById(R.id.btnOptions);
// Wire controls
@@ -113,18 +133,51 @@ public class FragmentSetup extends FragmentEx {
}
});
btnDoze.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
new AlertDialog.Builder(getContext())
.setMessage(R.string.title_setup_doze_instructions)
.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
try {
startActivity(new Intent(Settings.ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS));
} catch (Throwable ex) {
Log.e(Helper.TAG, ex + "\n" + Log.getStackTraceString(ex));
}
}
})
.create()
.show();
}
});
btnData.setOnClickListener(new View.OnClickListener() {
@Override
@TargetApi(Build.VERSION_CODES.N)
public void onClick(View v) {
try {
startActivity(new Intent(Settings.ACTION_IGNORE_BACKGROUND_DATA_RESTRICTIONS_SETTINGS,
Uri.parse("package:" + BuildConfig.APPLICATION_ID)));
} catch (Throwable ex) {
Log.e(Helper.TAG, ex + "\n" + Log.getStackTraceString(ex));
}
}
});
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getContext());
String theme = prefs.getString("theme", "light");
boolean dark = "dark".equals(theme);
cbDarkTheme.setTag(dark);
cbDarkTheme.setChecked(dark);
cbDarkTheme.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
tbDarkTheme.setTag(dark);
tbDarkTheme.setChecked(dark);
tbDarkTheme.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
@Override
public void onCheckedChanged(CompoundButton button, boolean checked) {
if (checked != (Boolean) button.getTag()) {
button.setTag(checked);
cbDarkTheme.setChecked(checked);
tbDarkTheme.setChecked(checked);
prefs.edit().putString("theme", checked ? "dark" : "light").apply();
}
}
@@ -141,11 +194,21 @@ public class FragmentSetup extends FragmentEx {
// Initialize
pbAccount.setVisibility(View.GONE);
pbIdentity.setVisibility(View.GONE);
tvAccountDone.setText(R.string.title_setup_to_do);
tvIdentityDone.setText(R.string.title_setup_to_do);
tvPermissionsDone.setText(R.string.title_setup_to_do);
tvAccountDone.setText(null);
tvAccountDone.setCompoundDrawables(null, null, null, null);
btnIdentity.setEnabled(false);
tvIdentityDone.setText(null);
tvIdentityDone.setCompoundDrawables(null, null, null, null);
tvPermissionsDone.setText(null);
tvPermissionsDone.setCompoundDrawables(null, null, null, null);
btnDoze.setEnabled(false);
tvDozeDone.setText(null);
tvDozeDone.setCompoundDrawables(null, null, null, null);
btnData.setVisibility(View.GONE);
int[] grantResults = new int[permissions.length];
for (int i = 0; i < permissions.length; i++)
@@ -197,18 +260,40 @@ public class FragmentSetup extends FragmentEx {
db.account().liveAccounts(true).observe(getViewLifecycleOwner(), new Observer<List<EntityAccount>>() {
@Override
public void onChanged(@Nullable List<EntityAccount> accounts) {
tvAccountDone.setText(accounts != null && accounts.size() > 0 ? R.string.title_setup_done : R.string.title_setup_to_do);
boolean done = (accounts != null && accounts.size() > 0);
btnIdentity.setEnabled(done);
tvAccountDone.setText(done ? R.string.title_setup_done : R.string.title_setup_to_do);
tvAccountDone.setCompoundDrawablesWithIntrinsicBounds(done ? check : null, null, null, null);
}
});
db.identity().liveIdentities(true).observe(getViewLifecycleOwner(), new Observer<List<EntityIdentity>>() {
@Override
public void onChanged(@Nullable List<EntityIdentity> identities) {
tvIdentityDone.setText(identities != null && identities.size() > 0 ? R.string.title_setup_done : R.string.title_setup_to_do);
boolean done = (identities != null && identities.size() > 0);
tvIdentityDone.setText(done ? R.string.title_setup_done : R.string.title_setup_to_do);
tvIdentityDone.setCompoundDrawablesWithIntrinsicBounds(done ? check : null, null, null, null);
}
});
}
@Override
public void onResume() {
super.onResume();
PowerManager pm = getContext().getSystemService(PowerManager.class);
boolean ignoring = pm.isIgnoringBatteryOptimizations(BuildConfig.APPLICATION_ID);
btnDoze.setEnabled(!ignoring);
tvDozeDone.setText(ignoring ? R.string.title_setup_done : R.string.title_setup_to_do);
tvDozeDone.setCompoundDrawablesWithIntrinsicBounds(ignoring ? check : null, null, null, null);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
ConnectivityManager cm = getContext().getSystemService(ConnectivityManager.class);
boolean saving = (cm.getRestrictBackgroundStatus() == ConnectivityManager.RESTRICT_BACKGROUND_STATUS_ENABLED);
btnData.setVisibility(saving ? View.VISIBLE : View.GONE);
}
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
boolean has = (grantResults.length > 0);
@@ -220,5 +305,6 @@ public class FragmentSetup extends FragmentEx {
btnPermissions.setEnabled(!has);
tvPermissionsDone.setText(has ? R.string.title_setup_done : R.string.title_setup_to_do);
tvPermissionsDone.setCompoundDrawablesWithIntrinsicBounds(has ? check : null, null, null, null);
}
}

View File

@@ -19,7 +19,10 @@ package eu.faircode.email;
Copyright 2018 by Marcel Bokhorst (M66B)
*/
import android.content.SharedPreferences;
import android.net.Uri;
import android.os.Bundle;
import android.preference.PreferenceManager;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
@@ -31,34 +34,43 @@ import android.widget.ProgressBar;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.browser.customtabs.CustomTabsIntent;
// https://developer.android.com/reference/android/webkit/WebView
public class FragmentWebView extends FragmentEx {
private String url = null;
@Override
@Nullable
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_webview, container, false);
final ProgressBar progressBar = view.findViewById(R.id.progressbar);
WebView webview = view.findViewById(R.id.webview);
final WebView webview = view.findViewById(R.id.webview);
progressBar.setProgress(0);
progressBar.setVisibility(View.VISIBLE);
WebSettings settings = webview.getSettings();
settings.setJavaScriptEnabled(true);
settings.setLoadWithOverviewMode(true);
settings.setUseWideViewPort(true);
settings.setMixedContentMode(WebSettings.MIXED_CONTENT_ALWAYS_ALLOW);
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getContext());
webview.setWebViewClient(new WebViewClient() {
public boolean shouldOverrideUrlLoading(WebView view, String url) {
view.loadUrl(url);
setSubtitle(url);
return false;
if (prefs.getBoolean("webview", false)) {
view.loadUrl(url);
setSubtitle(url);
return false;
} else {
CustomTabsIntent.Builder builder = new CustomTabsIntent.Builder();
builder.setToolbarColor(Helper.resolveColor(getContext(), R.attr.colorPrimary));
CustomTabsIntent customTabsIntent = builder.build();
customTabsIntent.launchUrl(getContext(), Uri.parse(url));
return true;
}
}
});
@@ -71,9 +83,31 @@ public class FragmentWebView extends FragmentEx {
});
Bundle args = getArguments();
url = (args == null ? null : args.getString("link"));
webview.loadUrl(url);
setSubtitle(url);
if (args.containsKey("url")) {
String url = args.getString("url");
webview.loadUrl(url);
setSubtitle(url);
} else if (args.containsKey("html")) {
String html = args.getString("html");
String from = args.getString("from");
webview.loadDataWithBaseURL("email://", html, "text/html", "UTF-8", null);
setSubtitle(from);
}
((ActivityBase) getActivity()).addBackPressedListener(new ActivityBase.IBackPressedListener() {
@Override
public boolean onBackPressed() {
boolean can = webview.canGoBack();
if (can)
webview.goBack();
Bundle args = getArguments();
if (args.containsKey("from") && !webview.canGoBack())
setSubtitle(args.getString("from"));
return can;
}
});
return view;
}

View File

@@ -23,6 +23,7 @@ import android.accounts.Account;
import android.accounts.AccountManager;
import android.content.Context;
import android.content.res.TypedArray;
import android.support.annotation.NonNull;
import android.text.TextUtils;
import android.util.Log;
import android.view.Menu;
@@ -47,16 +48,32 @@ import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.concurrent.ThreadFactory;
import javax.mail.Address;
import javax.mail.internet.InternetAddress;
import static android.os.Process.THREAD_PRIORITY_BACKGROUND;
public class Helper {
static final String TAG = "fairemail";
static final int JOB_DAILY = 1001;
static final int AUTH_TYPE_PASSWORD = 1;
static final int AUTH_TYPE_GMAIL = 2;
static final int ATTACHMENT_BUFFER_SIZE = 8192; // bytes
static ThreadFactory backgroundThreadFactory = new ThreadFactory() {
@Override
public Thread newThread(@NonNull Runnable runnable) {
Thread thread = new Thread(runnable);
thread.setPriority(THREAD_PRIORITY_BACKGROUND);
return thread;
}
};
static int resolveColor(Context context, int attr) {
int[] attrs = new int[]{attr};
TypedArray a = context.getTheme().obtainStyledAttributes(attrs);
@@ -92,10 +109,10 @@ public class Helper {
static String formatThrowable(Throwable ex) {
StringBuilder sb = new StringBuilder();
sb.append(ex.getMessage());
sb.append(ex.getMessage() == null ? ex.getClass().getName() : ex.getMessage());
Throwable cause = ex.getCause();
while (cause != null) {
sb.append(" ").append(cause.getMessage());
sb.append(" ").append(cause.getMessage() == null ? cause.getClass().getName() : cause.getMessage());
cause = cause.getCause();
}
return sb.toString();
@@ -118,7 +135,7 @@ public class Helper {
String[] cmd = new String[]{"logcat",
"-d",
"-v", "threadtime",
"-t", "500",
"-t", "1000",
TAG + ":I"};
proc = Runtime.getRuntime().exec(cmd);
br = new BufferedReader(new InputStreamReader(proc.getInputStream()));

View File

@@ -45,7 +45,8 @@ public class HtmlHelper implements NodeVisitor {
private String newline;
private List<String> refs = new ArrayList<>();
private StringBuilder sb = new StringBuilder();
private Pattern pattern = Pattern.compile("([http|https]+://[\\w\\S(\\.|:|/)]+)");
private static Pattern pattern = Pattern.compile("([http|https]+://[\\w\\S(\\.|:|/)]+)");
private HtmlHelper(Context context, boolean reply) {
this.context = context;
@@ -56,7 +57,6 @@ public class HtmlHelper implements NodeVisitor {
String name = node.nodeName();
if (node instanceof TextNode) {
String text = ((TextNode) node).text();
text = Html.escapeHtml(text);
Matcher matcher = pattern.matcher(text);
while (matcher.find()) {
String ref = matcher.group();
@@ -125,6 +125,25 @@ public class HtmlHelper implements NodeVisitor {
Document document = Jsoup.parse(Jsoup.clean(html, Whitelist.relaxed()));
for (Element tr : document.select("tr"))
tr.after("<br>");
NodeTraversor.traverse(new NodeVisitor() {
@Override
public void head(Node node, int depth) {
if (node instanceof TextNode) {
String text = ((TextNode) node).text();
Matcher matcher = pattern.matcher(text);
while (matcher.find()) {
String ref = matcher.group();
text = text.replace(ref, String.format("<a href=\"%s\">%s</a>", ref, ref));
}
node.before(text);
((TextNode) node).text("");
}
}
@Override
public void tail(Node node, int depth) {
}
}, document.body());
return document.body().html();
}
}

View File

@@ -0,0 +1,115 @@
package eu.faircode.email;
/*
This file is part of FairEmail.
FairEmail is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
NetGuard is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with NetGuard. If not, see <http://www.gnu.org/licenses/>.
Copyright 2018 by Marcel Bokhorst (M66B)
*/
import android.app.job.JobInfo;
import android.app.job.JobParameters;
import android.app.job.JobScheduler;
import android.app.job.JobService;
import android.content.ComponentName;
import android.content.Context;
import android.util.Log;
import java.io.File;
import java.util.Date;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class JobDaily extends JobService {
private ExecutorService executor = Executors.newSingleThreadExecutor(Helper.backgroundThreadFactory);
public static void schedule(Context context) {
Log.i(Helper.TAG, "Scheduling daily job");
JobInfo.Builder job = new JobInfo.Builder(Helper.JOB_DAILY, new ComponentName(context, JobDaily.class))
.setPeriodic(24 * 3600 * 1000L)
.setRequiresDeviceIdle(true);
JobScheduler scheduler = context.getSystemService(JobScheduler.class);
scheduler.cancel(Helper.JOB_DAILY);
if (scheduler.schedule(job.build()) == JobScheduler.RESULT_SUCCESS)
Log.i(Helper.TAG, "Scheduled daily job");
else
Log.e(Helper.TAG, "Failed to schedule daily job");
}
@Override
public boolean onStartJob(JobParameters args) {
Log.i(Helper.TAG, "Starting daily job");
EntityLog.log(this, "Daily cleanup");
final DB db = DB.getInstance(this);
executor.submit(new Runnable() {
@Override
public void run() {
Log.i(Helper.TAG, "Start daily job");
// Cleanup message files
Log.i(Helper.TAG, "Cleanup message files");
File[] messages = new File(getFilesDir(), "messages").listFiles();
if (messages != null)
for (File file : messages)
if (file.isFile()) {
long id = Long.parseLong(file.getName());
if (db.message().countMessage(id) == 0) {
Log.i(Helper.TAG, "Cleanup message id=" + id);
if (!file.delete())
Log.w(Helper.TAG, "Error deleting " + file);
}
}
// Cleanup attachment files
Log.i(Helper.TAG, "Cleanup attachment files");
File[] attachments = new File(getFilesDir(), "attachments").listFiles();
if (attachments != null)
for (File file : attachments)
if (file.isFile()) {
long id = Long.parseLong(file.getName());
if (db.attachment().countAttachment(id) == 0) {
Log.i(Helper.TAG, "Cleanup attachment id=" + id);
if (!file.delete())
Log.w(Helper.TAG, "Error deleting " + file);
}
}
Log.i(Helper.TAG, "Cleanup log");
long before = new Date().getTime() - 24 * 3600 * 1000L;
int logs = db.log().deleteLogs(before);
Log.i(Helper.TAG, "Deleted logs=" + logs);
// Cleanup found messages
Log.i(Helper.TAG, "Cleanup found messages");
int found = db.message().deleteFoundMessages();
Log.i(Helper.TAG, "Deleted found messages=" + found);
Log.i(Helper.TAG, "End daily job");
}
});
return false;
}
@Override
public boolean onStopJob(JobParameters args) {
return false;
}
}

View File

@@ -25,6 +25,7 @@ import android.preference.PreferenceManager;
import android.text.TextUtils;
import android.util.Base64;
import android.util.Log;
import android.webkit.MimeTypeMap;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
@@ -54,6 +55,7 @@ import javax.mail.internet.InternetAddress;
import javax.mail.internet.MimeBodyPart;
import javax.mail.internet.MimeMessage;
import javax.mail.internet.MimeMultipart;
import javax.mail.internet.ParseException;
public class MessageHelper {
private MimeMessage imessage;
@@ -212,6 +214,10 @@ public class MessageHelper {
return imessage.isSet(Flags.Flag.SEEN);
}
boolean getFlagged() throws MessagingException {
return imessage.isSet(Flags.Flag.FLAGGED);
}
String getMessageID() throws MessagingException {
return imessage.getHeader("Message-ID", null);
}
@@ -268,10 +274,13 @@ public class MessageHelper {
String personal = a.getPersonal();
if (TextUtils.isEmpty(personal))
formatted.add(address.toString());
else if (full)
formatted.add(personal + " <" + a.getAddress() + ">");
else
formatted.add(personal);
else {
personal = personal.replaceAll("[\\,\\<\\>]", "");
if (full)
formatted.add(personal + " <" + a.getAddress() + ">");
else
formatted.add(personal);
}
} else
formatted.add(address.toString());
return TextUtils.join(", ", formatted);
@@ -281,7 +290,7 @@ public class MessageHelper {
return getHtml(imessage);
}
private String getHtml(Part part) throws MessagingException, IOException {
private static String getHtml(Part part) throws MessagingException, IOException {
if (part.isMimeType("text/*")) {
String s;
try {
@@ -296,6 +305,10 @@ public class MessageHelper {
for (int len = is.read(buffer); len != -1; len = is.read(buffer))
os.write(buffer, 0, len);
s = new String(os.toByteArray(), "US-ASCII");
} catch (IOException ex) {
// IOException; Unknown encoding: none
Log.w(Helper.TAG, ex + "\n" + Log.getStackTraceString(ex));
s = ex.toString();
}
if (part.isMimeType("text/plain"))
@@ -319,15 +332,15 @@ public class MessageHelper {
} else
return getHtml(bp);
}
} catch (UnsupportedEncodingException ex) {
throw ex;
} catch (IOException ex) {
} catch (ParseException ex) {
// ParseException: In parameter list boundary="...">, expected parameter name, got ";"
Log.w(Helper.TAG, ex + "\n" + Log.getStackTraceString(ex));
text = ex.toString();
}
return text;
}
if (part.isMimeType("multipart/*")) {
if (part.isMimeType("multipart/*"))
try {
Multipart mp = (Multipart) part.getContent();
for (int i = 0; i < mp.getCount(); i++) {
@@ -335,12 +348,10 @@ public class MessageHelper {
if (s != null)
return s;
}
} catch (UnsupportedEncodingException ex) {
throw ex;
} catch (IOException ex) {
} catch (ParseException ex) {
Log.w(Helper.TAG, ex + "\n" + Log.getStackTraceString(ex));
return ex.toString();
}
}
return null;
}
@@ -348,20 +359,24 @@ public class MessageHelper {
public List<EntityAttachment> getAttachments() throws IOException, MessagingException {
List<EntityAttachment> result = new ArrayList<>();
Object content = imessage.getContent();
if (content instanceof String)
return result;
try {
Object content = imessage.getContent();
if (content instanceof String)
return result;
if (content instanceof Multipart) {
Multipart multipart = (Multipart) content;
for (int i = 0; i < multipart.getCount(); i++)
result.addAll(getAttachments(multipart.getBodyPart(i)));
if (content instanceof Multipart) {
Multipart multipart = (Multipart) content;
for (int i = 0; i < multipart.getCount(); i++)
result.addAll(getAttachments(multipart.getBodyPart(i)));
}
} catch (ParseException ex) {
Log.w(Helper.TAG, ex + "\n" + Log.getStackTraceString(ex));
}
return result;
}
private List<EntityAttachment> getAttachments(BodyPart part) throws
private static List<EntityAttachment> getAttachments(BodyPart part) throws
IOException, MessagingException {
List<EntityAttachment> result = new ArrayList<>();
@@ -371,12 +386,17 @@ public class MessageHelper {
} catch (UnsupportedEncodingException ex) {
Log.w(Helper.TAG, "attachment content type=" + part.getContentType());
content = part.getInputStream();
} catch (ParseException ex) {
Log.w(Helper.TAG, ex + "\n" + Log.getStackTraceString(ex));
content = null;
}
if (content instanceof InputStream || content instanceof String) {
String disposition;
try {
disposition = part.getDisposition();
} catch (MessagingException ex) {
Log.w(Helper.TAG, ex + "\n" + Log.getStackTraceString(ex));
disposition = null;
}
@@ -384,6 +404,7 @@ public class MessageHelper {
try {
filename = part.getFileName();
} catch (MessagingException ex) {
Log.w(Helper.TAG, ex + "\n" + Log.getStackTraceString(ex));
filename = null;
}
@@ -394,8 +415,23 @@ public class MessageHelper {
attachment.type = ct.getBaseType();
attachment.size = part.getSize();
attachment.part = part;
// Try to guess a better content type
// Sometimes PDF files are sent using the wrong type
if ("application/octet-stream".equals(attachment.type) && attachment.name != null) {
String extension = MimeTypeMap.getFileExtensionFromUrl(attachment.name.toLowerCase());
if (extension != null) {
String type = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension);
if (type != null) {
Log.w(Helper.TAG, "Guessing file=" + attachment.name + " type=" + type);
attachment.type = type;
}
}
}
if (attachment.size < 0)
attachment.size = null;
result.add(attachment);
}
} else if (content instanceof Multipart) {

View File

@@ -1,241 +0,0 @@
package eu.faircode.email;
import android.content.Context;
import android.os.Handler;
import android.os.Looper;
import android.text.TextUtils;
import android.util.Log;
import android.util.SparseArray;
import android.widget.Toast;
import com.sun.mail.imap.IMAPFolder;
import com.sun.mail.imap.IMAPStore;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.List;
import java.util.Properties;
import javax.mail.FetchProfile;
import javax.mail.Folder;
import javax.mail.Message;
import javax.mail.MessagingException;
import javax.mail.Session;
import javax.mail.UIDFolder;
import javax.mail.internet.MimeMessage;
import javax.mail.search.BodyTerm;
import javax.mail.search.FromStringTerm;
import javax.mail.search.OrTerm;
import javax.mail.search.SubjectTerm;
import androidx.lifecycle.Lifecycle;
import androidx.lifecycle.LifecycleObserver;
import androidx.lifecycle.LifecycleOwner;
import androidx.lifecycle.OnLifecycleEvent;
import androidx.paging.PositionalDataSource;
public class SearchDataSource extends PositionalDataSource<TupleMessageEx> implements LifecycleObserver {
private Context context;
private LifecycleOwner owner;
private long fid;
private String search;
private EntityFolder folder;
private EntityAccount account;
private IMAPStore istore = null;
private IMAPFolder ifolder;
private Message[] imessages;
private SparseArray<TupleMessageEx> cache = new SparseArray<>();
private static final float MAX_MEMORY_USAGE = 0.6f; // percent
SearchDataSource(Context context, LifecycleOwner owner, long folder, String search) {
Log.i(Helper.TAG, "SDS create");
this.context = context;
this.owner = owner;
this.fid = folder;
this.search = search;
owner.getLifecycle().addObserver(this);
}
@OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
public void onDestroyed() {
Log.i(Helper.TAG, "SDS destroy");
new Thread(new Runnable() {
@Override
public void run() {
try {
if (istore != null)
istore.close();
} catch (MessagingException ex) {
Log.e(Helper.TAG, ex + "\n" + Log.getStackTraceString(ex));
} finally {
istore = null;
ifolder = null;
imessages = null;
cache.clear();
}
}
}).start();
owner.getLifecycle().removeObserver(this);
}
@Override
public void loadInitial(LoadInitialParams params, LoadInitialCallback<TupleMessageEx> callback) {
Log.i(Helper.TAG, "SDS load initial");
try {
SearchResult result = search(search, params.requestedStartPosition, params.requestedLoadSize);
callback.onResult(result.messages, params.requestedStartPosition, result.total);
} catch (Throwable ex) {
Log.e(Helper.TAG, ex + "\n" + Log.getStackTraceString(ex));
reportError(ex);
}
}
@Override
public void loadRange(LoadRangeParams params, LoadRangeCallback<TupleMessageEx> callback) {
Log.i(Helper.TAG, "SDS load range");
try {
SearchResult result = search(search, params.startPosition, params.loadSize);
callback.onResult(result.messages);
} catch (Throwable ex) {
Log.e(Helper.TAG, ex + "\n" + Log.getStackTraceString(ex));
reportError(ex);
}
}
private SearchResult search(String term, int from, int count) throws MessagingException, IOException {
Log.i(Helper.TAG, "SDS search from=" + from + " count=" + count);
if (istore == null) {
DB db = DB.getInstance(context);
folder = db.folder().getFolder(fid);
account = db.account().getAccount(folder.account);
// Refresh token
if (account.auth_type == Helper.AUTH_TYPE_GMAIL) {
account.password = Helper.refreshToken(context, "com.google", account.user, account.password);
db.account().setAccountPassword(account.id, account.password);
}
Properties props = MessageHelper.getSessionProperties(context, account.auth_type);
props.setProperty("mail.imap.throwsearchexception", "true");
Session isession = Session.getInstance(props, null);
Log.i(Helper.TAG, "SDS connecting account=" + account.name);
istore = (IMAPStore) isession.getStore("imaps");
istore.connect(account.host, account.port, account.user, account.password);
Log.i(Helper.TAG, "SDS opening folder=" + folder.name);
ifolder = (IMAPFolder) istore.getFolder(folder.name);
ifolder.open(Folder.READ_WRITE);
Log.i(Helper.TAG, "SDS searching term=" + term);
imessages = ifolder.search(
new OrTerm(
new FromStringTerm(term),
new OrTerm(
new SubjectTerm(term),
new BodyTerm(term))));
Log.i(Helper.TAG, "SDS found messages=" + imessages.length);
}
SearchResult result = new SearchResult();
result.total = imessages.length;
result.messages = new ArrayList<>();
List<Message> selected = new ArrayList<>();
int base = imessages.length - 1 - from;
for (int i = base; i >= 0 && i >= base - count + 1; i--)
selected.add(imessages[i]);
Log.i(Helper.TAG, "SDS selected messages=" + selected.size());
FetchProfile fp = new FetchProfile();
fp.add(UIDFolder.FetchProfileItem.UID);
fp.add(IMAPFolder.FetchProfileItem.FLAGS);
fp.add(FetchProfile.Item.ENVELOPE);
fp.add(FetchProfile.Item.CONTENT_INFO);
fp.add(IMAPFolder.FetchProfileItem.HEADERS);
fp.add(IMAPFolder.FetchProfileItem.MESSAGE);
ifolder.fetch(selected.toArray(new Message[0]), fp);
for (int s = 0; s < selected.size(); s++) {
int pos = from + s;
if (cache.get(pos) != null) {
Log.i(Helper.TAG, "SDS from cache pos=" + pos);
result.messages.add(cache.get(pos));
continue;
}
Message imessage = selected.get(s);
long uid = ifolder.getUID(imessage);
MessageHelper helper = new MessageHelper((MimeMessage) imessage);
boolean seen = helper.getSeen();
TupleMessageEx message = new TupleMessageEx();
message.id = uid;
message.account = folder.account;
message.folder = folder.id;
message.uid = uid;
message.msgid = helper.getMessageID();
message.references = TextUtils.join(" ", helper.getReferences());
message.inreplyto = helper.getInReplyTo();
message.thread = helper.getThreadId(uid);
message.from = helper.getFrom();
message.to = helper.getTo();
message.cc = helper.getCc();
message.bcc = helper.getBcc();
message.reply = helper.getReply();
message.subject = imessage.getSubject();
message.received = imessage.getReceivedDate().getTime();
message.sent = (imessage.getSentDate() == null ? null : imessage.getSentDate().getTime());
message.seen = seen;
message.ui_seen = seen;
message.ui_hide = false;
message.accountName = account.name;
message.folderName = folder.name;
message.folderType = folder.type;
message.count = 1;
message.unseen = (seen ? 0 : 1);
message.attachments = 0;
message.body = helper.getHtml();
message.virtual = true;
result.messages.add(message);
Runtime rt = Runtime.getRuntime();
float used = (float) (rt.totalMemory() - rt.freeMemory()) / rt.maxMemory();
if (used < MAX_MEMORY_USAGE)
cache.put(pos, message);
else
Log.i(Helper.TAG, "SDS memory used=" + used);
}
Log.i(Helper.TAG, "SDS result=" + result.messages.size());
return result;
}
private void reportError(final Throwable ex) {
new Handler(Looper.getMainLooper()).post(new Runnable() {
@Override
public void run() {
Toast.makeText(context, Helper.formatThrowable(ex), Toast.LENGTH_LONG).show();
}
});
}
private class SearchResult {
int total;
List<TupleMessageEx> messages;
}
}

View File

@@ -59,12 +59,15 @@ import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.SocketException;
import java.net.SocketTimeoutException;
import java.net.UnknownHostException;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@@ -79,12 +82,14 @@ import javax.mail.Flags;
import javax.mail.Folder;
import javax.mail.FolderClosedException;
import javax.mail.FolderNotFoundException;
import javax.mail.Header;
import javax.mail.Message;
import javax.mail.MessageRemovedException;
import javax.mail.MessagingException;
import javax.mail.NoSuchProviderException;
import javax.mail.SendFailedException;
import javax.mail.Session;
import javax.mail.StoreClosedException;
import javax.mail.Transport;
import javax.mail.UIDFolder;
import javax.mail.event.ConnectionAdapter;
@@ -99,7 +104,6 @@ import javax.mail.event.StoreEvent;
import javax.mail.event.StoreListener;
import javax.mail.internet.InternetAddress;
import javax.mail.internet.MimeMessage;
import javax.mail.internet.ParseException;
import javax.mail.search.ComparisonTerm;
import javax.mail.search.ReceivedDateTerm;
import javax.net.ssl.SSLException;
@@ -119,10 +123,10 @@ public class ServiceSynchronize extends LifecycleService {
private static final int NOTIFICATION_SYNCHRONIZE = 1;
private static final int NOTIFICATION_UNSEEN = 2;
private static final int CONNECT_BACKOFF_START = 32; // seconds
private static final int CONNECT_BACKOFF_START = 8; // seconds
private static final int CONNECT_BACKOFF_MAX = 1024; // seconds (1024 sec ~ 17 min)
private static final long STORE_NOOP_INTERVAL = 9 * 60 * 1000L; // ms
private static final int ATTACHMENT_BUFFER_SIZE = 8192; // bytes
private static final int ATTACHMENT_AUTO_DOWNLOAD_SIZE = 32 * 1024; // bytes
static final String ACTION_SYNCHRONIZE_FOLDER = BuildConfig.APPLICATION_ID + ".SYNCHRONIZE_FOLDER";
static final String ACTION_PROCESS_OPERATIONS = BuildConfig.APPLICATION_ID + ".PROCESS_OPERATIONS";
@@ -305,7 +309,7 @@ public class ServiceSynchronize extends LifecycleService {
if (!TextUtils.isEmpty(message.subject))
sb.append(": ").append(message.subject);
sb.append(" ").append(df.format(new Date(message.sent)));
sb.append("\n");
sb.append("<br>");
}
builder.setStyle(new Notification.BigTextStyle().bigText(Html.fromHtml(sb.toString())));
@@ -339,6 +343,8 @@ public class ServiceSynchronize extends LifecycleService {
.setCategory(Notification.CATEGORY_ERROR)
.setVisibility(Notification.VISIBILITY_SECRET);
builder.setStyle(new Notification.BigTextStyle().bigText(ex.toString()));
return builder;
}
@@ -355,37 +361,44 @@ public class ServiceSynchronize extends LifecycleService {
// MailConnectException
// - on connectity problems when connecting to store
String action;
if (TextUtils.isEmpty(account))
action = folder;
else if (TextUtils.isEmpty(folder))
action = account;
else
action = account + "/" + folder;
EntityLog.log(this, action + "\n" + ex.toString() + "\n" + Log.getStackTraceString(ex));
if (!(ex instanceof MailConnectException) &&
!(ex instanceof FolderClosedException) &&
!(ex instanceof IllegalStateException) &&
!(ex instanceof AuthenticationFailedException) && // Also: Too many simultaneous connections
!(ex instanceof StoreClosedException) &&
!(ex instanceof MessagingException && ex.getCause() instanceof UnknownHostException) &&
!(ex instanceof MessagingException && ex.getCause() instanceof ConnectionException) &&
!(ex instanceof MessagingException && ex.getCause() instanceof SocketException) &&
!(ex instanceof MessagingException && ex.getCause() instanceof SocketTimeoutException) &&
!(ex instanceof MessagingException && ex.getCause() instanceof SSLException)) {
String action;
if (TextUtils.isEmpty(account))
action = folder;
else if (TextUtils.isEmpty(folder))
action = account;
else
action = account + "/" + folder;
!(ex instanceof MessagingException && ex.getCause() instanceof SSLException) &&
!(ex instanceof MessagingException && "connection failure".equals(ex.getMessage()))) {
NotificationManager nm = getSystemService(NotificationManager.class);
nm.notify(action, 1, getNotificationError(action, ex).build());
}
}
private void monitorAccount(final EntityAccount account, final ServiceState state) throws NoSuchProviderException {
Log.i(Helper.TAG, account.name + " start");
final DB db = DB.getInstance(this);
final ExecutorService executor = Executors.newSingleThreadExecutor();
final ExecutorService executor = Executors.newSingleThreadExecutor(Helper.backgroundThreadFactory);
int backoff = CONNECT_BACKOFF_START;
while (state.running) {
Log.i(Helper.TAG, account.name + " run");
EntityLog.log(this, account.name + " run");
// Debug
boolean debug = PreferenceManager.getDefaultSharedPreferences(this).getBoolean("debug", false);
if (debug)
System.setProperty("mail.socket.debug", "true");
System.setProperty("mail.socket.debug", Boolean.toString(debug));
// Refresh token
if (account.auth_type == Helper.AUTH_TYPE_GMAIL) {
@@ -393,6 +406,7 @@ public class ServiceSynchronize extends LifecycleService {
db.account().setAccountPassword(account.id, account.password);
}
// Create session
Properties props = MessageHelper.getSessionProperties(this, account.auth_type);
final Session isession = Session.getInstance(props, null);
isession.setDebug(debug);
@@ -459,19 +473,15 @@ public class ServiceSynchronize extends LifecycleService {
db.folder().setFolderState(folder.id, null);
db.account().setAccountState(account.id, "connecting");
istore.connect(account.host, account.port, account.user, account.password);
boolean hasIdle = istore.hasCapability("IDLE");
backoff = CONNECT_BACKOFF_START;
db.account().setAccountState(account.id, "connected");
db.account().setAccountError(account.id, null);
EntityLog.log(this, account.name + " connected");
// Update folder list
try {
synchronizeFolders(account, istore, state);
} catch (MessagingException ex) {
// Don't show to user
throw new IllegalStateException("synchronize folders", ex);
}
synchronizeFolders(account, istore, state);
// Synchronize folders
for (final EntityFolder folder : db.folder().getFolders(account.id, true)) {
@@ -480,7 +490,12 @@ public class ServiceSynchronize extends LifecycleService {
db.folder().setFolderState(folder.id, "connecting");
final IMAPFolder ifolder = (IMAPFolder) istore.getFolder(folder.name);
ifolder.open(Folder.READ_WRITE);
try {
ifolder.open(Folder.READ_WRITE);
} catch (Throwable ex) {
db.folder().setFolderError(folder.id, Helper.formatThrowable(ex));
throw ex;
}
folders.put(folder, ifolder);
db.folder().setFolderState(folder.id, "connected");
@@ -491,6 +506,9 @@ public class ServiceSynchronize extends LifecycleService {
@Override
public void run() {
try {
// Process pending operations
processOperations(folder, isession, istore, ifolder);
// Listen for new and deleted messages
ifolder.addMessageCountListener(new MessageCountAdapter() {
@Override
@@ -500,10 +518,16 @@ public class ServiceSynchronize extends LifecycleService {
Log.i(Helper.TAG, folder.name + " messages added");
for (Message imessage : e.getMessages())
try {
synchronizeMessage(folder, ifolder, (IMAPMessage) imessage);
synchronizeMessage(ServiceSynchronize.this, folder, ifolder, (IMAPMessage) imessage, false);
} catch (MessageRemovedException ex) {
Log.w(Helper.TAG, folder.name + " " + ex + "\n" + Log.getStackTraceString(ex));
} catch (IOException ex) {
if (ex.getCause() instanceof MessageRemovedException)
Log.w(Helper.TAG, folder.name + " " + ex + "\n" + Log.getStackTraceString(ex));
else
throw ex;
}
EntityOperation.process(ServiceSynchronize.this); // download small attachments
} catch (Throwable ex) {
Log.e(Helper.TAG, folder.name + " " + ex + "\n" + Log.getStackTraceString(ex));
reportError(account.name, folder.name, ex);
@@ -560,9 +584,15 @@ public class ServiceSynchronize extends LifecycleService {
try {
try {
Log.i(Helper.TAG, folder.name + " message changed");
synchronizeMessage(folder, ifolder, (IMAPMessage) e.getMessage());
synchronizeMessage(ServiceSynchronize.this, folder, ifolder, (IMAPMessage) e.getMessage(), false);
EntityOperation.process(ServiceSynchronize.this); // download small attachments
} catch (MessageRemovedException ex) {
Log.w(Helper.TAG, folder.name + " " + ex + "\n" + Log.getStackTraceString(ex));
} catch (IOException ex) {
if (ex.getCause() instanceof MessageRemovedException)
Log.w(Helper.TAG, folder.name + " " + ex + "\n" + Log.getStackTraceString(ex));
else
throw ex;
}
} catch (Throwable ex) {
Log.e(Helper.TAG, folder.name + " " + ex + "\n" + Log.getStackTraceString(ex));
@@ -583,18 +613,15 @@ public class ServiceSynchronize extends LifecycleService {
try {
Thread.sleep(account.poll_interval * 60 * 1000L);
if (istore.hasCapability("IDLE")) {
Log.i(Helper.TAG, folder.name + " request NOOP");
ifolder.doCommand(new IMAPFolder.ProtocolCommand() {
public Object doCommand(IMAPProtocol p) throws ProtocolException {
Log.i(Helper.TAG, ifolder.getName() + " start NOOP");
p.simpleCommand("NOOP", null);
Log.i(Helper.TAG, ifolder.getName() + " end NOOP");
return null;
}
});
} else
synchronizeMessages(account, folder, ifolder, state);
Log.i(Helper.TAG, folder.name + " request NOOP");
ifolder.doCommand(new IMAPFolder.ProtocolCommand() {
public Object doCommand(IMAPProtocol p) throws ProtocolException {
Log.i(Helper.TAG, ifolder.getName() + " start NOOP");
p.simpleCommand("NOOP", null);
Log.i(Helper.TAG, ifolder.getName() + " end NOOP");
return null;
}
});
} catch (InterruptedException ex) {
Log.w(Helper.TAG, folder.name + " noop " + ex.toString());
@@ -618,34 +645,32 @@ public class ServiceSynchronize extends LifecycleService {
noops.add(noop);
// Receive folder events
if (hasIdle) {
Thread idle = new Thread(new Runnable() {
@Override
public void run() {
try {
Log.i(Helper.TAG, folder.name + " start idle");
while (state.running && ifolder.isOpen()) {
Log.i(Helper.TAG, folder.name + " do idle");
ifolder.idle(false);
Log.i(Helper.TAG, folder.name + " done idle");
}
} catch (Throwable ex) {
Log.e(Helper.TAG, folder.name + " " + ex + "\n" + Log.getStackTraceString(ex));
reportError(account.name, folder.name, ex);
db.folder().setFolderError(folder.id, Helper.formatThrowable(ex));
synchronized (state) {
state.notifyAll();
}
} finally {
Log.i(Helper.TAG, folder.name + " end idle");
Thread idle = new Thread(new Runnable() {
@Override
public void run() {
try {
Log.i(Helper.TAG, folder.name + " start idle");
while (state.running && ifolder.isOpen()) {
Log.i(Helper.TAG, folder.name + " do idle");
ifolder.idle(false);
Log.i(Helper.TAG, folder.name + " done idle");
}
} catch (Throwable ex) {
Log.e(Helper.TAG, folder.name + " " + ex + "\n" + Log.getStackTraceString(ex));
reportError(account.name, folder.name, ex);
db.folder().setFolderError(folder.id, Helper.formatThrowable(ex));
synchronized (state) {
state.notifyAll();
}
} finally {
Log.i(Helper.TAG, folder.name + " end idle");
}
}, "sync.idle." + folder.id);
idle.start();
idlers.add(idle);
}
}
}, "sync.idle." + folder.id);
idle.start();
idlers.add(idle);
}
BroadcastReceiver processFolder = new BroadcastReceiver() {
@@ -673,10 +698,7 @@ public class ServiceSynchronize extends LifecycleService {
if (folder == null)
folder = db.folder().getFolder(fid);
if (shouldClose)
Log.i(Helper.TAG, folder.name + " run offline=" + shouldClose);
else
Log.i(Helper.TAG, folder.name + " run online");
Log.i(Helper.TAG, folder.name + " run " + (shouldClose ? "offline" : "online"));
if (ifolder == null) {
// Prevent unnecessary folder connections
@@ -684,28 +706,37 @@ public class ServiceSynchronize extends LifecycleService {
if (db.operation().getOperationCount(fid) == 0)
return;
db.folder().setFolderState(folder.id, "connecting");
ifolder = (IMAPFolder) istore.getFolder(folder.name);
ifolder.open(Folder.READ_WRITE);
db.folder().setFolderState(folder.id, "connected");
db.folder().setFolderError(folder.id, null);
}
if (ACTION_PROCESS_OPERATIONS.equals(intent.getAction()))
processOperations(folder, isession, istore, ifolder);
else if (ACTION_SYNCHRONIZE_FOLDER.equals(intent.getAction()))
synchronizeMessages(account, folder, ifolder, state);
} catch (Throwable ex) {
Log.e(Helper.TAG, folder.name + " " + ex + "\n" + Log.getStackTraceString(ex));
reportError(account.name, folder.name, ex);
db.folder().setFolderError(folder.id, Helper.formatThrowable(ex));
} finally {
if (shouldClose)
if (shouldClose) {
if (ifolder != null && ifolder.isOpen()) {
db.folder().setFolderState(folder.id, "closing");
try {
ifolder.close(false);
} catch (MessagingException ex) {
Log.w(Helper.TAG, folder.name + " " + ex + "\n" + Log.getStackTraceString(ex));
}
}
db.folder().setFolderState(folder.id, null);
}
}
}
});
@@ -721,14 +752,6 @@ public class ServiceSynchronize extends LifecycleService {
lbm.registerReceiver(processFolder, f);
try {
// Process pending folder operations
Log.i(Helper.TAG, "listen process folder");
for (final EntityFolder folder : folders.keySet())
if (!EntityFolder.OUTBOX.equals(folder.type))
lbm.sendBroadcast(new Intent(ACTION_PROCESS_OPERATIONS)
.setType("account/" + account.id)
.putExtra("folder", folder.id));
// Keep store alive
while (state.running && istore.isConnected()) {
Log.i(Helper.TAG, "Checking folders");
@@ -753,8 +776,7 @@ public class ServiceSynchronize extends LifecycleService {
}
} catch (Throwable ex) {
Log.e(Helper.TAG, account.name + " " + ex + "\n" + Log.getStackTraceString(ex));
if (!(ex instanceof AuthenticationFailedException)) // Also: Too many simultaneous connections
reportError(account.name, null, ex);
reportError(account.name, null, ex);
db.account().setAccountError(account.id, Helper.formatThrowable(ex));
} finally {
// Close store
@@ -762,6 +784,7 @@ public class ServiceSynchronize extends LifecycleService {
db.account().setAccountState(account.id, "closing");
for (EntityFolder folder : folders.keySet())
db.folder().setFolderState(folder.id, "closing");
EntityLog.log(this, account.name + " closing");
try {
// This can take some time
istore.close();
@@ -769,6 +792,7 @@ public class ServiceSynchronize extends LifecycleService {
Log.w(Helper.TAG, account.name + " " + ex + "\n" + Log.getStackTraceString(ex));
} finally {
Log.i(Helper.TAG, account.name + " closed");
EntityLog.log(this, account.name + " closed");
db.account().setAccountState(account.id, null);
for (EntityFolder folder : folders.keySet())
db.folder().setFolderState(folder.id, null);
@@ -789,7 +813,8 @@ public class ServiceSynchronize extends LifecycleService {
if (state.running) {
try {
Log.i(Helper.TAG, "Backoff seconds=" + backoff);
Log.i(Helper.TAG, account.name + " backoff=" + backoff);
EntityLog.log(this, account.name + " backoff=" + backoff);
Thread.sleep(backoff * 1000L);
if (backoff < CONNECT_BACKOFF_MAX)
@@ -801,6 +826,7 @@ public class ServiceSynchronize extends LifecycleService {
}
Log.i(Helper.TAG, account.name + " stopped");
EntityLog.log(this, account.name + " stopped");
}
private void processOperations(EntityFolder folder, Session isession, IMAPStore istore, IMAPFolder ifolder) throws MessagingException, JSONException, IOException {
@@ -828,7 +854,8 @@ public class ServiceSynchronize extends LifecycleService {
if (message.uid == null &&
(EntityOperation.SEEN.equals(op.name) ||
EntityOperation.DELETE.equals(op.name) ||
EntityOperation.MOVE.equals(op.name)))
EntityOperation.MOVE.equals(op.name) ||
EntityOperation.HEADERS.equals(op.name)))
throw new IllegalArgumentException(op.name + " without uid");
JSONArray jargs = new JSONArray(op.args);
@@ -836,6 +863,9 @@ public class ServiceSynchronize extends LifecycleService {
if (EntityOperation.SEEN.equals(op.name))
doSeen(folder, ifolder, message, jargs, db);
else if (EntityOperation.FLAG.equals(op.name))
doFlag(folder, ifolder, message, jargs, db);
else if (EntityOperation.ADD.equals(op.name))
doAdd(folder, isession, ifolder, message, jargs, db);
@@ -851,6 +881,9 @@ public class ServiceSynchronize extends LifecycleService {
else if (EntityOperation.ATTACHMENT.equals(op.name))
doAttachment(folder, op, ifolder, message, jargs, db);
else if (EntityOperation.HEADERS.equals(op.name))
doHeaders(folder, ifolder, message, db);
else
throw new MessagingException("Unknown operation name=" + op.name);
@@ -903,24 +936,34 @@ public class ServiceSynchronize extends LifecycleService {
db.message().setMessageSeen(message.id, seen);
}
private void doFlag(EntityFolder folder, IMAPFolder ifolder, EntityMessage message, JSONArray jargs, DB db) throws MessagingException, JSONException {
// Star/unstar message
boolean flagged = jargs.getBoolean(0);
Message imessage = ifolder.getMessageByUID(message.uid);
if (imessage == null)
throw new MessageRemovedException();
imessage.setFlag(Flags.Flag.FLAGGED, flagged);
db.message().setMessageFlagged(message.id, flagged);
}
private void doAdd(EntityFolder folder, Session isession, IMAPFolder ifolder, EntityMessage message, JSONArray jargs, DB db) throws MessagingException, JSONException, IOException {
// Append message
List<EntityAttachment> attachments = db.attachment().getAttachments(message.id);
MimeMessage imessage = MessageHelper.from(this, message, attachments, isession);
AppendUID[] uid = ifolder.appendUIDMessages(new Message[]{imessage});
db.message().setMessageUid(message.id, uid[0].uid);
Log.i(Helper.TAG, "Appended uid=" + uid[0].uid);
if (message.uid != null) {
Message iprev = ifolder.getMessageByUID(message.uid);
if (iprev != null) {
Log.i(Helper.TAG, "Deleting existing id=" + message.id);
Log.i(Helper.TAG, "Deleting existing uid=" + message.uid);
iprev.setFlag(Flags.Flag.DELETED, true);
ifolder.expunge();
}
}
db.message().setMessageUid(message.id, uid[0].uid);
Log.i(Helper.TAG, "Appended uid=" + message.uid);
}
private void doMove(EntityFolder folder, Session isession, IMAPStore istore, IMAPFolder ifolder, EntityMessage message, JSONArray jargs, DB db) throws JSONException, MessagingException, IOException {
@@ -1080,7 +1123,7 @@ public class ServiceSynchronize extends LifecycleService {
os = new BufferedOutputStream(new FileOutputStream(file));
int size = 0;
byte[] buffer = new byte[ATTACHMENT_BUFFER_SIZE];
byte[] buffer = new byte[Helper.ATTACHMENT_BUFFER_SIZE];
for (int len = is.read(buffer); len != -1; len = is.read(buffer)) {
size += len;
os.write(buffer, 0, len);
@@ -1113,11 +1156,23 @@ public class ServiceSynchronize extends LifecycleService {
}
}
private void synchronizeFolders(EntityAccount account, IMAPStore istore, ServiceState state) throws MessagingException {
try {
Log.v(Helper.TAG, "Start sync folders");
private void doHeaders(EntityFolder folder, IMAPFolder ifolder, EntityMessage message, DB db) throws MessagingException {
Message imessage = ifolder.getMessageByUID(message.uid);
Enumeration<Header> headers = imessage.getAllHeaders();
StringBuilder sb = new StringBuilder();
while (headers.hasMoreElements()) {
Header header = headers.nextElement();
sb.append(header.getName()).append(": ").append(header.getValue()).append("\n");
}
db.message().setMessageHeaders(message.id, sb.toString());
}
DB db = DB.getInstance(this);
private void synchronizeFolders(EntityAccount account, IMAPStore istore, ServiceState state) throws MessagingException {
DB db = DB.getInstance(this);
try {
db.beginTransaction();
Log.v(Helper.TAG, "Start sync folders");
List<String> names = new ArrayList<>();
for (EntityFolder folder : db.folder().getUserFolders(account.id))
@@ -1128,9 +1183,6 @@ public class ServiceSynchronize extends LifecycleService {
Log.i(Helper.TAG, "Remote folder count=" + ifolders.length);
for (Folder ifolder : ifolders) {
if (!state.running)
return;
String[] attrs = ((IMAPFolder) ifolder).getAttributes();
boolean selectable = true;
for (String attr : attrs) {
@@ -1165,7 +1217,10 @@ public class ServiceSynchronize extends LifecycleService {
Log.i(Helper.TAG, "Delete local folder=" + names.size());
for (String name : names)
db.folder().deleteFolder(account.id, name);
db.setTransactionSuccessful();
} finally {
db.endTransaction();
Log.v(Helper.TAG, "End sync folder");
}
}
@@ -1243,41 +1298,23 @@ public class ServiceSynchronize extends LifecycleService {
Log.i(Helper.TAG, folder.name + " add=" + imessages.length);
for (int i = imessages.length - 1; i >= 0; i--)
try {
int status = synchronizeMessage(folder, ifolder, (IMAPMessage) imessages[i]);
int status = synchronizeMessage(this, folder, ifolder, (IMAPMessage) imessages[i], false);
if (status > 0)
added++;
else if (status < 0)
updated++;
else
unchanged++;
} catch (ParseException ex) {
Log.e(Helper.TAG, folder.name + " " + ex + "\n" + Log.getStackTraceString(ex));
reportError(account.name, folder.name, ex);
} catch (MessageRemovedException ex) {
Log.w(Helper.TAG, folder.name + " " + ex + "\n" + Log.getStackTraceString(ex));
} catch (IOException ex) {
// Getting attachments
if (ex.getCause() instanceof MessageRemovedException)
Log.w(Helper.TAG, folder.name + " " + ex + "\n" + Log.getStackTraceString(ex));
else
throw ex;
}
// Cleanup files
File[] messages = new File(getFilesDir(), "messages").listFiles();
if (messages != null)
for (File file : messages)
if (file.isFile()) {
long id = Long.parseLong(file.getName());
if (db.message().countMessage(id) == 0) {
Log.i(Helper.TAG, "Cleanup message id=" + id);
file.delete();
}
}
File[] attachments = new File(getFilesDir(), "attachments").listFiles();
if (attachments != null)
for (File file : attachments)
if (file.isFile()) {
long id = Long.parseLong(file.getName());
if (db.attachment().countAttachment(id) == 0) {
Log.i(Helper.TAG, "Cleanup attachment id=" + id);
file.delete();
}
}
EntityOperation.process(this); // download small attachments
Log.w(Helper.TAG, folder.name + " statistics added=" + added + " updated=" + updated + " unchanged=" + unchanged);
} finally {
@@ -1286,7 +1323,7 @@ public class ServiceSynchronize extends LifecycleService {
}
}
private int synchronizeMessage(EntityFolder folder, IMAPFolder ifolder, IMAPMessage imessage) throws MessagingException, IOException {
static int synchronizeMessage(Context context, EntityFolder folder, IMAPFolder ifolder, IMAPMessage imessage, boolean found) throws MessagingException, IOException {
long uid;
try {
FetchProfile fp = new FetchProfile();
@@ -1299,17 +1336,20 @@ public class ServiceSynchronize extends LifecycleService {
if (imessage.isExpunged()) {
Log.i(Helper.TAG, folder.name + " expunged uid=" + uid);
imessage.invalidateHeaders();
return 0;
}
if (imessage.isSet(Flags.Flag.DELETED)) {
Log.i(Helper.TAG, folder.name + " deleted uid=" + uid);
imessage.invalidateHeaders();
return 0;
}
MessageHelper helper = new MessageHelper(imessage);
boolean seen = helper.getSeen();
boolean flagged = helper.getFlagged();
DB db = DB.getInstance(this);
DB db = DB.getInstance(context);
try {
int result = 0;
@@ -1351,6 +1391,13 @@ public class ServiceSynchronize extends LifecycleService {
Log.i(Helper.TAG, folder.name + " updated id=" + message.id + " uid=" + message.uid + " seen=" + seen);
result = -1;
}
if (message.flagged != flagged || message.flagged != message.ui_flagged) {
message.flagged = flagged;
message.ui_flagged = flagged;
db.message().updateMessage(message);
Log.i(Helper.TAG, folder.name + " updated id=" + message.id + " uid=" + message.uid + " flagged=" + flagged);
result = -1;
}
}
if (message == null) {
@@ -1386,12 +1433,18 @@ public class ServiceSynchronize extends LifecycleService {
message.sent = (imessage.getSentDate() == null ? null : imessage.getSentDate().getTime());
message.seen = seen;
message.ui_seen = seen;
message.flagged = false;
message.ui_flagged = false;
message.ui_hide = false;
message.ui_found = found;
message.id = db.message().insertMessage(message);
message.write(this, helper.getHtml());
message.write(context, helper.getHtml());
Log.i(Helper.TAG, folder.name + " added id=" + message.id + " uid=" + message.uid);
// Free memory
imessage.invalidateHeaders();
int sequence = 0;
for (EntityAttachment attachment : helper.getAttachments()) {
sequence++;
@@ -1400,6 +1453,9 @@ public class ServiceSynchronize extends LifecycleService {
attachment.message = message.id;
attachment.sequence = sequence;
attachment.id = db.attachment().insertAttachment(attachment);
if (attachment.size != null && attachment.size < ATTACHMENT_AUTO_DOWNLOAD_SIZE)
EntityOperation.queue(db, message, EntityOperation.ATTACHMENT, sequence);
}
result = 1;
@@ -1420,12 +1476,15 @@ public class ServiceSynchronize extends LifecycleService {
private boolean running = false;
private Thread main;
private EntityFolder outbox = null;
private ExecutorService lifecycle = Executors.newSingleThreadExecutor();
private ExecutorService executor = Executors.newSingleThreadExecutor();
private ExecutorService lifecycle = Executors.newSingleThreadExecutor(Helper.backgroundThreadFactory);
private ExecutorService executor = Executors.newSingleThreadExecutor(Helper.backgroundThreadFactory);
@Override
public void onAvailable(Network network) {
Log.i(Helper.TAG, "Network available " + network);
ConnectivityManager cm = getSystemService(ConnectivityManager.class);
NetworkInfo ni = cm.getNetworkInfo(network);
Log.i(Helper.TAG, "Network available " + network + " " + ni);
EntityLog.log(ServiceSynchronize.this, "Network available " + network + " " + ni);
if (running)
Log.i(Helper.TAG, "Service already running");
@@ -1445,14 +1504,16 @@ public class ServiceSynchronize extends LifecycleService {
@Override
public void onLost(Network network) {
Log.i(Helper.TAG, "Network lost " + network);
EntityLog.log(ServiceSynchronize.this, "Network lost " + network);
if (running) {
Log.i(Helper.TAG, "Service running");
ConnectivityManager cm = getSystemService(ConnectivityManager.class);
NetworkInfo ni = cm.getActiveNetworkInfo();
Log.i(Helper.TAG, "Network active=" + (ni == null ? null : ni.toString()));
if (ni == null || !ni.isConnected()) {
Log.i(Helper.TAG, "Network disconnected=" + ni);
NetworkInfo ani = cm.getActiveNetworkInfo();
Log.i(Helper.TAG, "Network active=" + (ani == null ? null : ani.toString()));
if (ani == null || !ani.isConnected()) {
EntityLog.log(ServiceSynchronize.this, "Network disconnected=" + ani);
Log.i(Helper.TAG, "Network disconnected=" + ani);
running = false;
lifecycle.submit(new Runnable() {
@Override
@@ -1467,6 +1528,7 @@ public class ServiceSynchronize extends LifecycleService {
}
private void start() {
EntityLog.log(ServiceSynchronize.this, "Main start");
state = new ServiceState();
main = new Thread(new Runnable() {
@@ -1522,15 +1584,29 @@ public class ServiceSynchronize extends LifecycleService {
threads.add(t);
}
EntityLog.log(ServiceSynchronize.this, "Main started");
synchronized (state) {
try {
state.wait();
} catch (InterruptedException ex) {
Log.w(Helper.TAG, "main wait " + ex.toString());
}
}
// Stop monitoring accounts
for (Thread t : threads)
for (Thread t : threads) {
t.interrupt();
join(t);
}
threads.clear();
// Stop monitoring outbox
lbm.unregisterReceiver(outboxReceiver);
Log.i(Helper.TAG, outbox.name + " unlisten operations");
db.folder().setFolderState(outbox.id, null);
EntityLog.log(ServiceSynchronize.this, "Main exited");
} catch (Throwable ex) {
// Fail-safe
Log.e(Helper.TAG, ex + "\n" + Log.getStackTraceString(ex));
@@ -1543,6 +1619,7 @@ public class ServiceSynchronize extends LifecycleService {
private void stop(boolean disconnected) {
if (main != null) {
EntityLog.log(ServiceSynchronize.this, "Main stop disconnected=" + disconnected);
synchronized (state) {
state.running = false;
state.disconnected = disconnected;
@@ -1554,6 +1631,8 @@ public class ServiceSynchronize extends LifecycleService {
join(main);
main = null;
EntityLog.log(ServiceSynchronize.this, "Main stopped");
}
}
@@ -1592,7 +1671,7 @@ public class ServiceSynchronize extends LifecycleService {
reportError(null, outbox.name, ex);
} finally {
Log.i(Helper.TAG, outbox.name + " end operations");
db.folder().setFolderState(outbox.id, "connected");
db.folder().setFolderState(outbox.id, null);
}
}
});
@@ -1610,12 +1689,12 @@ public class ServiceSynchronize extends LifecycleService {
Log.i(Helper.TAG, "Joined " + thread.getName());
} catch (InterruptedException ex) {
Log.e(Helper.TAG, thread.getName() + " join " + ex.toString());
thread.interrupt();
}
}
public static void start(Context context) {
ContextCompat.startForegroundService(context, new Intent(context, ServiceSynchronize.class));
JobDaily.schedule(context);
}
public static void reload(Context context, String reason) {

View File

@@ -22,9 +22,11 @@ package eu.faircode.email;
import android.content.Context;
import android.os.Bundle;
import android.os.Handler;
import android.os.HandlerThread;
import android.util.Log;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import androidx.appcompat.app.AppCompatActivity;
import androidx.fragment.app.Fragment;
import androidx.lifecycle.Lifecycle;
@@ -33,8 +35,6 @@ import androidx.lifecycle.LifecycleOwner;
import androidx.lifecycle.LifecycleService;
import androidx.lifecycle.OnLifecycleEvent;
import static android.os.Process.THREAD_PRIORITY_BACKGROUND;
//
// This simple task is simple to use, but it is also simple to cause bugs that can easily lead to crashes
// Make sure to not access any member in any outer scope from onLoad
@@ -47,15 +47,7 @@ public abstract class SimpleTask<T> implements LifecycleObserver {
private Bundle args = null;
private Result stored = null;
private static HandlerThread handlerThread;
private static Handler handler;
static {
handlerThread = new HandlerThread("SimpleTask");
handlerThread.start();
handlerThread.setPriority(THREAD_PRIORITY_BACKGROUND);
handler = new Handler(handlerThread.getLooper());
}
private ExecutorService executor = Executors.newCachedThreadPool(Helper.backgroundThreadFactory);
public void load(Context context, LifecycleOwner owner, Bundle args) {
run(context, owner, args);
@@ -70,7 +62,11 @@ public abstract class SimpleTask<T> implements LifecycleObserver {
}
public void load(final Fragment fragment, Bundle args) {
run(fragment.getContext(), fragment.getViewLifecycleOwner(), args);
try {
run(fragment.getContext(), fragment.getViewLifecycleOwner(), args);
} catch (IllegalStateException ex) {
Log.w(Helper.TAG, ex + "\n" + Log.getStackTraceString(ex));
}
}
@OnLifecycleEvent(Lifecycle.Event.ON_START)
@@ -120,7 +116,7 @@ public abstract class SimpleTask<T> implements LifecycleObserver {
owner.getLifecycle().addObserver(this);
// Run in background thread
handler.post(new Runnable() {
executor.submit(new Runnable() {
@Override
public void run() {
final Result result = new Result();

View File

@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M9,16.17L4.83,12l-1.42,1.41L9,19 21,7l-1.41,-1.41z"/>
</vector>

View File

@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M20,6h-8l-2,-2L4,4c-1.1,0 -2,0.9 -2,2v12c0,1.1 0.9,2 2,2h16c1.1,0 2,-0.9 2,-2L22,8c0,-1.1 -0.9,-2 -2,-2zM17.94,17L15,15.28 12.06,17l0.78,-3.33 -2.59,-2.24 3.41,-0.29L15,8l1.34,3.14 3.41,0.29 -2.59,2.24 0.78,3.33z"/>
</vector>

View File

@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M19,3L4.99,3c-1.11,0 -1.98,0.89 -1.98,2L3,19c0,1.1 0.88,2 1.99,2L19,21c1.1,0 2,-0.9 2,-2L21,5c0,-1.11 -0.9,-2 -2,-2zM19,15h-4c0,1.66 -1.35,3 -3,3s-3,-1.34 -3,-3L4.99,15L4.99,5L19,5v10z"/>
</vector>

View File

@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M19,3L4.99,3c-1.11,0 -1.98,0.9 -1.98,2L3,19c0,1.1 0.88,2 1.99,2L19,21c1.1,0 2,-0.9 2,-2L21,5c0,-1.1 -0.9,-2 -2,-2zM19,15h-4c0,1.66 -1.35,3 -3,3s-3,-1.34 -3,-3L4.99,15L4.99,5L19,5v10zM16,10h-2L14,7h-4v3L8,10l4,4 4,-4z"/>
</vector>

View File

@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M22,9.24l-7.19,-0.62L12,2 9.19,8.63 2,9.24l5.46,4.73L5.82,21 12,17.27 18.18,21l-1.63,-7.03L22,9.24zM12,15.4l-3.76,2.27 1,-4.28 -3.32,-2.88 4.38,-0.38L12,6.1l1.71,4.04 4.38,0.38 -3.32,2.88 1,4.28L12,15.4z"/>
</vector>

View File

@@ -47,6 +47,18 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tvCopyright" />
<Button
android:id="@+id/btnLog"
style="?android:attr/buttonStyleSmall"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:minHeight="0dp"
android:minWidth="0dp"
android:text="@string/title_log"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tvEula" />
<Button
android:id="@+id/btnDebugInfo"
style="?android:attr/buttonStyleSmall"
@@ -56,8 +68,7 @@
android:minHeight="0dp"
android:minWidth="0dp"
android:text="@string/title_debug_info"
android:textAppearance="@style/TextAppearance.AppCompat.Small"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/tvEula" />
</androidx.constraintlayout.widget.ConstraintLayout>
</ScrollView>

View File

@@ -30,28 +30,6 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tvProvider" />
<!-- name -->
<TextView
android:id="@+id/tvName"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:text="@string/title_account_name"
android:textAppearance="@style/TextAppearance.AppCompat.Small"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/spProvider" />
<EditText
android:id="@+id/etName"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/title_account_name_hint"
android:inputType="text"
android:textAppearance="@style/TextAppearance.AppCompat.Medium"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tvName" />
<!-- IMAP -->
<TextView
@@ -62,7 +40,27 @@
android:text="@string/title_imap"
android:textAppearance="@style/TextAppearance.AppCompat.Medium"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/etName" />
app:layout_constraintTop_toBottomOf="@id/spProvider" />
<TextView
android:id="@+id/tvPop"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/title_pop"
android:textAppearance="@style/TextAppearance.AppCompat.Small"
android:textStyle="italic"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tvImap" />
<TextView
android:id="@+id/tvInsecure"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/title_insecure"
android:textAppearance="@style/TextAppearance.AppCompat.Small"
android:textStyle="italic"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tvPop" />
<!-- host -->
@@ -74,7 +72,7 @@
android:text="@string/title_host"
android:textAppearance="@style/TextAppearance.AppCompat.Small"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tvImap" />
app:layout_constraintTop_toBottomOf="@id/tvInsecure" />
<EditText
android:id="@+id/etHost"
@@ -167,27 +165,70 @@
android:inputType="textPassword" />
</com.google.android.material.textfield.TextInputLayout>
<TextView
android:id="@+id/tvInstructions"
<Button
android:id="@+id/btnAdvanced"
style="@style/buttonStyleSmall"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:freezesText="true"
android:text="@string/title_instructions"
android:textAppearance="@style/TextAppearance.AppCompat.Small"
android:minHeight="0dp"
android:minWidth="0dp"
android:text="@string/title_setup_advanced"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tilPassword" />
<!-- name -->
<TextView
android:id="@+id/tvLink"
android:id="@+id/tvName"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:freezesText="true"
android:text="link"
android:text="@string/title_account_name"
android:textAppearance="@style/TextAppearance.AppCompat.Small"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tvInstructions" />
app:layout_constraintTop_toBottomOf="@id/btnAdvanced" />
<EditText
android:id="@+id/etName"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/title_account_name_hint"
android:inputType="text"
android:textAppearance="@style/TextAppearance.AppCompat.Medium"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tvName" />
<TextView
android:id="@+id/tvSignature"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:text="@string/title_account_signature"
android:textAppearance="@style/TextAppearance.AppCompat.Small"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/etName" />
<EditText
android:id="@+id/etSignature"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:hint="@string/title_optional"
android:inputType="textCapSentences|textMultiLine"
android:textAppearance="@style/TextAppearance.AppCompat.Medium"
app:layout_constraintEnd_toStartOf="@+id/ibPro"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tvSignature" />
<ImageButton
android:id="@+id/ibPro"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="6dp"
android:src="@drawable/baseline_info_24"
app:layout_constraintBottom_toBottomOf="@id/etSignature"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@id/etSignature" />
<CheckBox
android:id="@+id/cbSynchronize"
@@ -196,7 +237,7 @@
android:layout_marginTop="12dp"
android:text="@string/title_synchronize_account"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tvLink" />
app:layout_constraintTop_toBottomOf="@id/etSignature" />
<CheckBox
android:id="@+id/cbPrimary"
@@ -207,26 +248,6 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/cbSynchronize" />
<TextView
android:id="@+id/tvInterval"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:text="@string/title_interval"
android:textAppearance="@style/TextAppearance.AppCompat.Small"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/cbPrimary" />
<EditText
android:id="@+id/etInterval"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="number"
android:text="9"
android:textAppearance="@style/TextAppearance.AppCompat.Medium"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tvInterval" />
<!-- check -->
<Button
@@ -236,7 +257,7 @@
android:layout_marginTop="12dp"
android:text="@string/title_check"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/etInterval" />
app:layout_constraintTop_toBottomOf="@id/cbPrimary" />
<ProgressBar
android:id="@+id/pbCheck"
@@ -256,19 +277,7 @@
android:layout_marginTop="12dp"
android:src="@drawable/baseline_delete_24"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/etInterval" />
<TextView
android:id="@+id/tvIdle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:minWidth="100dp"
android:text="@string/title_no_idle"
android:textAppearance="@style/TextAppearance.AppCompat.Small"
android:textStyle="bold"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/btnCheck" />
app:layout_constraintTop_toBottomOf="@id/cbPrimary" />
<TextView
android:id="@+id/tvDrafts"
@@ -288,7 +297,7 @@
android:layout_marginTop="12dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/tvDrafts"
app:layout_constraintTop_toBottomOf="@id/tvIdle" />
app:layout_constraintTop_toBottomOf="@id/btnCheck" />
<TextView
android:id="@+id/tvSent"
@@ -404,10 +413,22 @@
app:layout_constraintTop_toTopOf="parent" />
<androidx.constraintlayout.widget.Group
android:id="@+id/grpInstructions"
android:id="@+id/grpServer"
android:layout_width="0dp"
android:layout_height="0dp"
app:constraint_referenced_ids="tvInstructions,tvLink" />
app:constraint_referenced_ids="tvImap,tvPop,tvInsecure,tvHost,etHost,tvPort,etPort" />
<androidx.constraintlayout.widget.Group
android:id="@+id/grpAuthorize"
android:layout_width="0dp"
android:layout_height="0dp"
app:constraint_referenced_ids="tvUser,etUser,tvPassword,tilPassword" />
<androidx.constraintlayout.widget.Group
android:id="@+id/grpAdvanced"
android:layout_width="0dp"
android:layout_height="0dp"
app:constraint_referenced_ids="tvName,etName,tvSignature,etSignature,ibPro,cbSynchronize,cbPrimary" />
<androidx.constraintlayout.widget.Group
android:id="@+id/grpFolders"

View File

@@ -22,6 +22,15 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<CheckBox
android:id="@+id/cbUnified"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:text="@string/title_unified_folder"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/cbSynchronize" />
<!-- after -->
<TextView
@@ -32,7 +41,7 @@
android:text="@string/title_after"
android:textAppearance="@style/TextAppearance.AppCompat.Small"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/cbSynchronize" />
app:layout_constraintTop_toBottomOf="@id/cbUnified" />
<EditText
android:id="@+id/etAfter"

View File

@@ -32,6 +32,37 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tvName" />
<!--- linked account -->
<TextView
android:id="@+id/tvAccount"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:text="@string/title_account_linked"
android:textAppearance="@style/TextAppearance.AppCompat.Small"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/etName" />
<Spinner
android:id="@+id/spAccount"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tvAccount" />
<Button
android:id="@+id/btnAdvanced"
style="@style/buttonStyleSmall"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:minHeight="0dp"
android:minWidth="0dp"
android:text="@string/title_setup_advanced"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/spAccount" />
<!-- email -->
<TextView
@@ -42,7 +73,7 @@
android:text="@string/title_identity_email"
android:textAppearance="@style/TextAppearance.AppCompat.Small"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/etName" />
app:layout_constraintTop_toBottomOf="@id/btnAdvanced" />
<EditText
android:id="@+id/etEmail"
@@ -75,26 +106,6 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tvReplyTo" />
<!--- linked account -->
<TextView
android:id="@+id/tvAccount"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:text="@string/title_account_linked"
android:textAppearance="@style/TextAppearance.AppCompat.Small"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/etReplyTo" />
<Spinner
android:id="@+id/spAccount"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tvAccount" />
<!--- provider -->
<TextView
@@ -105,7 +116,7 @@
android:text="@string/title_provider"
android:textAppearance="@style/TextAppearance.AppCompat.Small"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/spAccount" />
app:layout_constraintTop_toBottomOf="@id/etReplyTo" />
<Spinner
android:id="@+id/spProvider"
@@ -126,6 +137,16 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/spProvider" />
<TextView
android:id="@+id/tvInsecure"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/title_insecure"
android:textAppearance="@style/TextAppearance.AppCompat.Small"
android:textStyle="italic"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tvSmtp" />
<!-- host -->
<TextView
@@ -136,7 +157,7 @@
android:text="@string/title_host"
android:textAppearance="@style/TextAppearance.AppCompat.Small"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tvSmtp" />
app:layout_constraintTop_toBottomOf="@id/tvInsecure" />
<EditText
android:id="@+id/etHost"
@@ -230,28 +251,6 @@
android:inputType="textPassword" />
</com.google.android.material.textfield.TextInputLayout>
<TextView
android:id="@+id/tvInstructions"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:freezesText="true"
android:text="@string/title_instructions"
android:textAppearance="@style/TextAppearance.AppCompat.Small"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tilPassword" />
<TextView
android:id="@+id/tvLink"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:freezesText="true"
android:text="link"
android:textAppearance="@style/TextAppearance.AppCompat.Small"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tvInstructions" />
<CheckBox
android:id="@+id/cbSynchronize"
android:layout_width="wrap_content"
@@ -259,7 +258,7 @@
android:layout_marginTop="12dp"
android:text="@string/title_synchronize_identity"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tvLink" />
app:layout_constraintTop_toBottomOf="@id/tilPassword" />
<CheckBox
android:id="@+id/cbPrimary"
@@ -320,9 +319,9 @@
app:layout_constraintTop_toTopOf="parent" />
<androidx.constraintlayout.widget.Group
android:id="@+id/grpInstructions"
android:id="@+id/grpAdvanced"
android:layout_width="0dp"
android:layout_height="0dp"
app:constraint_referenced_ids="tvInstructions,tvLink" />
app:constraint_referenced_ids="tvEmail,etEmail,tvReplyTo,etReplyTo,tvProvider,spProvider,tvSmtp,tvInsecure,tvHost,etHost,cbStartTls,tvPort,etPort,tvUser,etUser,tvPassword,tilPassword,cbSynchronize,cbPrimary,cbStoreSent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</ScrollView>

View File

@@ -48,6 +48,26 @@
app:layout_constraintStart_toEndOf="@id/ivAttachment"
app:layout_constraintTop_toTopOf="@id/ivAttachment" />
<ImageView
android:id="@+id/ivThread"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_marginTop="12dp"
android:src="@drawable/baseline_message_24"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/ivAttachment" />
<TextView
android:id="@+id/tvThread"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="18dp"
android:text="@string/title_legend_thread"
android:textAppearance="@style/TextAppearance.AppCompat.Medium"
app:layout_constraintBottom_toBottomOf="@id/ivThread"
app:layout_constraintStart_toEndOf="@id/ivThread"
app:layout_constraintTop_toTopOf="@id/ivThread" />
<ImageView
android:id="@+id/ivSynchronize"
android:layout_width="24dp"
@@ -55,7 +75,7 @@
android:layout_marginTop="12dp"
android:src="@drawable/baseline_sync_24"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/ivAttachment" />
app:layout_constraintTop_toBottomOf="@id/ivThread" />
<TextView
android:id="@+id/tvSynchronize"
@@ -88,6 +108,26 @@
app:layout_constraintStart_toEndOf="@id/ivPrimary"
app:layout_constraintTop_toTopOf="@id/ivPrimary" />
<ImageView
android:id="@+id/ivUnified"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_marginTop="12dp"
android:src="@drawable/baseline_folder_special_24"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/ivPrimary" />
<TextView
android:id="@+id/tvUnified"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="18dp"
android:text="@string/title_legend_unified"
android:textAppearance="@style/TextAppearance.AppCompat.Medium"
app:layout_constraintBottom_toBottomOf="@id/ivUnified"
app:layout_constraintStart_toEndOf="@id/ivUnified"
app:layout_constraintTop_toTopOf="@id/ivUnified" />
<ImageView
android:id="@+id/ivDisconnected"
android:layout_width="24dp"
@@ -95,7 +135,7 @@
android:layout_marginTop="12dp"
android:src="@drawable/baseline_cloud_off_24"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/ivPrimary" />
app:layout_constraintTop_toBottomOf="@id/ivUnified" />
<TextView
android:id="@+id/tvDisconnected"

View File

@@ -0,0 +1,36 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".ActivityView">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rvLog"
android:layout_width="0dp"
android:layout_height="0dp"
android:scrollbarStyle="outsideOverlay"
android:scrollbars="vertical"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<ProgressBar
android:id="@+id/pbWait"
style="@style/Base.Widget.AppCompat.ProgressBar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:indeterminate="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<androidx.constraintlayout.widget.Group
android:id="@+id/grpReady"
android:layout_width="0dp"
android:layout_height="0dp"
app:constraint_referenced_ids="rvLog" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -14,6 +14,17 @@
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<ImageView
android:id="@+id/ivFlagged"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_marginStart="6dp"
android:src="@drawable/baseline_star_24"
android:visibility="visible"
app:layout_constraintBottom_toBottomOf="@id/tvFrom"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@+id/tvFrom" />
<TextView
android:id="@+id/tvFrom"
android:layout_width="0dp"
@@ -25,21 +36,9 @@
android:text="From"
android:textAppearance="@style/TextAppearance.AppCompat.Medium"
android:textIsSelectable="true"
app:layout_constraintEnd_toStartOf="@+id/tvSize"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/tvSize"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="6dp"
android:maxLines="1"
android:text="123 K"
android:textAppearance="@style/TextAppearance.AppCompat.Small"
android:textIsSelectable="true"
app:layout_constraintEnd_toStartOf="@+id/tvTime"
app:layout_constraintTop_toTopOf="@id/tvFrom" />
app:layout_constraintStart_toEndOf="@id/ivFlagged"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/tvTime"
@@ -98,10 +97,21 @@
android:id="@+id/tvCount"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="6dp"
android:layout_marginEnd="3dp"
android:maxLines="1"
android:text="3"
android:textAppearance="@style/TextAppearance.AppCompat.Small"
app:layout_constraintBottom_toBottomOf="@id/tvSubject"
app:layout_constraintEnd_toStartOf="@+id/ivThread"
app:layout_constraintTop_toTopOf="@id/tvSubject" />
<ImageView
android:id="@+id/ivThread"
android:layout_width="15dp"
android:layout_height="15dp"
android:layout_marginEnd="6dp"
android:src="@drawable/baseline_message_24"
app:layout_constraintBottom_toBottomOf="@id/tvSubject"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@id/tvSubject" />
@@ -186,6 +196,45 @@
app:layout_constraintStart_toEndOf="@id/tvBccTitle"
app:layout_constraintTop_toBottomOf="@id/tvCc" />
<View
android:id="@+id/vSeparatorRawHeaders"
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginTop="3dp"
android:background="?attr/colorSeparator"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tvBcc" />
<TextView
android:id="@+id/tvRawHeaders"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginEnd="6dp"
android:layout_marginStart="6dp"
android:layout_marginTop="3dp"
android:fontFamily="monospace"
android:freezesText="true"
android:maxHeight="120sp"
android:text="H1\nH2\nH3\nH4\nH5\nH6\nH7\nH8\nH9\n"
android:textAppearance="@style/TextAppearance.AppCompat.Small"
android:textIsSelectable="true"
android:textSize="12sp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/vSeparatorRawHeaders" />
<ProgressBar
android:id="@+id/pbRawHeaders"
style="@style/Base.Widget.AppCompat.ProgressBar"
android:layout_width="24dp"
android:layout_height="24dp"
android:indeterminate="true"
app:layout_constraintBottom_toBottomOf="@id/tvRawHeaders"
app:layout_constraintEnd_toEndOf="@id/tvRawHeaders"
app:layout_constraintStart_toStartOf="@id/tvRawHeaders"
app:layout_constraintTop_toTopOf="@id/tvRawHeaders" />
<View
android:id="@+id/vSeparatorAttachments"
android:layout_width="match_parent"
@@ -193,7 +242,7 @@
android:layout_marginTop="3dp"
android:background="?attr/colorSeparator"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tvBcc" />
app:layout_constraintTop_toBottomOf="@id/tvRawHeaders" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rvAttachment"
@@ -253,7 +302,6 @@
android:minHeight="0dp"
android:minWidth="0dp"
android:text="@string/title_show_images"
android:textAppearance="@style/TextAppearance.AppCompat.Small"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/vSeparatorBody" />
@@ -285,11 +333,10 @@
style="@style/Base.Widget.AppCompat.ProgressBar"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_marginStart="12dp"
android:indeterminate="true"
app:layout_constraintBottom_toBottomOf="@id/scroll"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="@id/scroll"
app:layout_constraintStart_toStartOf="@id/scroll"
app:layout_constraintTop_toTopOf="@id/scroll" />
<com.google.android.material.floatingactionbutton.FloatingActionButton
@@ -335,12 +382,24 @@
android:layout_height="0dp"
app:constraint_referenced_ids="tvFrom,tvToTitle,tvTo,tvSize,tvTime,tvSubject" />
<androidx.constraintlayout.widget.Group
android:id="@+id/grpThread"
android:layout_width="0dp"
android:layout_height="0dp"
app:constraint_referenced_ids="tvCount,ivThread" />
<androidx.constraintlayout.widget.Group
android:id="@+id/grpAddresses"
android:layout_width="0dp"
android:layout_height="0dp"
app:constraint_referenced_ids="vSeparatorAddress,tvReplyToTitle,tvReplyTo,tvCcTitle,tvCc,tvBccTitle,tvBcc" />
<androidx.constraintlayout.widget.Group
android:id="@+id/grpRawHeaders"
android:layout_width="0dp"
android:layout_height="0dp"
app:constraint_referenced_ids="vSeparatorRawHeaders,tvRawHeaders" />
<androidx.constraintlayout.widget.Group
android:id="@+id/grpAttachments"
android:layout_width="0dp"

View File

@@ -18,28 +18,28 @@
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/tvHintSwipe"
android:id="@+id/tvHintActions"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="6dp"
android:layout_marginStart="6dp"
android:layout_marginTop="6dp"
android:text="@string/title_hint_swipe"
app:layout_constraintEnd_toStartOf="@+id/btnHintSwipe"
android:text="@string/title_hint_actions"
app:layout_constraintEnd_toStartOf="@+id/btnHintActions"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<Button
android:id="@+id/btnHintSwipe"
android:id="@+id/btnHintActions"
style="?android:attr/buttonStyleSmall"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:minHeight="0dp"
android:minWidth="0dp"
android:text="@string/title_understood"
app:layout_constraintBottom_toBottomOf="@id/tvHintSwipe"
app:layout_constraintBottom_toBottomOf="@id/tvHintActions"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@id/tvHintSwipe" />
app:layout_constraintTop_toTopOf="@id/tvHintActions" />
<View
android:id="@+id/vSeparator"
@@ -48,7 +48,7 @@
android:layout_marginTop="6dp"
android:background="?attr/colorSeparator"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/btnHintSwipe" />
app:layout_constraintTop_toBottomOf="@id/btnHintActions" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rvFolder"
@@ -88,7 +88,7 @@
android:id="@+id/grpHintSwipe"
android:layout_width="0dp"
android:layout_height="0dp"
app:constraint_referenced_ids="tvHintSwipe,btnHintSwipe,vSeparator" />
app:constraint_referenced_ids="tvHintActions,btnHintActions,vSeparator" />
<androidx.constraintlayout.widget.Group
android:id="@+id/grpReady"

View File

@@ -18,8 +18,6 @@
android:layout_marginStart="12dp"
android:layout_marginTop="12dp"
android:text="@string/title_advanced_webview"
android:textAppearance="@style/TextAppearance.AppCompat.Small"
android:textColor="?android:attr/textColorSecondary"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
@@ -40,8 +38,6 @@
android:layout_marginStart="12dp"
android:layout_marginTop="12dp"
android:text="@string/title_advanced_sanitize"
android:textAppearance="@style/TextAppearance.AppCompat.Small"
android:textColor="?android:attr/textColorSecondary"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tvCustomTabs" />
@@ -52,8 +48,6 @@
android:layout_marginStart="12dp"
android:layout_marginTop="12dp"
android:text="@string/title_advanced_compress_imap"
android:textAppearance="@style/TextAppearance.AppCompat.Small"
android:textColor="?android:attr/textColorSecondary"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/cbSanitize" />
@@ -64,8 +58,6 @@
android:layout_marginStart="12dp"
android:layout_marginTop="12dp"
android:text="@string/title_advanced_debug"
android:textAppearance="@style/TextAppearance.AppCompat.Small"
android:textColor="?android:attr/textColorSecondary"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/cbCompressImap" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -34,7 +34,6 @@
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:text="@string/title_pro_purchase"
android:textAppearance="@style/TextAppearance.AppCompat.Small"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tvList" />

View File

@@ -19,22 +19,10 @@
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:text="@string/title_setup_account"
android:textAppearance="@style/TextAppearance.AppCompat.Medium"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<ProgressBar
android:id="@+id/pbAccount"
style="@style/Base.Widget.AppCompat.ProgressBar"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_marginStart="12dp"
android:indeterminate="true"
app:layout_constraintBottom_toBottomOf="@id/btnAccount"
app:layout_constraintStart_toEndOf="@id/btnAccount"
app:layout_constraintTop_toTopOf="@id/btnAccount" />
<TextView
android:id="@+id/tvAccount"
android:layout_width="wrap_content"
@@ -49,6 +37,7 @@
android:id="@+id/tvAccountDone"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:drawableStart="@drawable/baseline_check_24"
android:text="@string/title_setup_done"
android:textAppearance="@style/TextAppearance.AppCompat.Medium"
android:textColor="?android:attr/textColorPrimary"
@@ -56,29 +45,26 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tvAccount" />
<View
android:id="@+id/vSeparatorAccount"
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginTop="9dp"
android:background="?attr/colorSeparator"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tvAccountDone" />
<!-- identity -->
<Button
android:id="@+id/btnIdentity"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:layout_marginTop="9dp"
android:text="@string/title_setup_identity"
android:textAppearance="@style/TextAppearance.AppCompat.Medium"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tvAccountDone" />
<ProgressBar
android:id="@+id/pbIdentity"
style="@style/Base.Widget.AppCompat.ProgressBar"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_marginStart="12dp"
android:indeterminate="true"
app:layout_constraintBottom_toBottomOf="@id/btnIdentity"
app:layout_constraintStart_toEndOf="@id/btnIdentity"
app:layout_constraintTop_toTopOf="@id/btnIdentity" />
app:layout_constraintTop_toBottomOf="@id/vSeparatorAccount" />
<TextView
android:id="@+id/tvIdentity"
@@ -94,6 +80,7 @@
android:id="@+id/tvIdentityDone"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:drawableStart="@drawable/baseline_check_24"
android:text="@string/title_setup_done"
android:textAppearance="@style/TextAppearance.AppCompat.Medium"
android:textColor="?android:attr/textColorPrimary"
@@ -101,18 +88,26 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tvIdentity" />
<View
android:id="@+id/vSeparatorIdentity"
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginTop="9dp"
android:background="?attr/colorSeparator"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tvIdentityDone" />
<!-- permissions -->
<Button
android:id="@+id/btnPermissions"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:layout_marginTop="9dp"
android:text="@string/title_setup_permissions"
android:textAppearance="@style/TextAppearance.AppCompat.Medium"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tvIdentityDone" />
app:layout_constraintTop_toBottomOf="@id/vSeparatorIdentity" />
<TextView
android:id="@+id/tvPermissions"
@@ -128,37 +123,103 @@
android:id="@+id/tvPermissionsDone"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/title_setup_to_do"
android:drawableStart="@drawable/baseline_check_24"
android:text="@string/title_setup_done"
android:textAppearance="@style/TextAppearance.AppCompat.Medium"
android:textColor="?android:attr/textColorPrimary"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tvPermissions" />
<CheckBox
android:id="@+id/cbDarkTheme"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:text="@string/title_setup_dark_theme"
android:textAppearance="@style/TextAppearance.AppCompat.Medium"
android:textColor="?android:attr/textColorSecondary"
app:layout_constraintEnd_toEndOf="parent"
<View
android:id="@+id/vSeparatorPermissions"
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginTop="9dp"
android:background="?attr/colorSeparator"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tvPermissionsDone" />
<!-- doze -->
<Button
android:id="@+id/btnDoze"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="9dp"
android:text="@string/title_setup_doze"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/vSeparatorPermissions" />
<TextView
android:id="@+id/tvDoze"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/title_setup_doze_remark"
android:textAppearance="@style/TextAppearance.AppCompat.Small"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/btnDoze" />
<TextView
android:id="@+id/tvDozeDone"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:drawableStart="@drawable/baseline_check_24"
android:text="@string/title_setup_done"
android:textAppearance="@style/TextAppearance.AppCompat.Medium"
android:textColor="?android:attr/textColorPrimary"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tvDoze" />
<View
android:id="@+id/vSeparatorDoze"
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginTop="9dp"
android:background="?attr/colorSeparator"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tvDozeDone" />
<!-- data saver -->
<Button
android:id="@+id/btnData"
style="?android:attr/buttonStyleSmall"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="9dp"
android:minHeight="0dp"
android:minWidth="0dp"
android:text="@string/title_setup_data"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/vSeparatorDoze" />
<ToggleButton
android:id="@+id/tbDarkTheme"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="18dp"
android:textOff="@string/title_setup_dark_theme"
android:textOn="@string/title_setup_light_theme"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/btnData" />
<Button
android:id="@+id/btnOptions"
style="?android:attr/buttonStyleSmall"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:layout_marginTop="18dp"
android:minHeight="0dp"
android:minWidth="0dp"
android:text="@string/title_advanced"
android:textAppearance="@style/TextAppearance.AppCompat.Small"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/cbDarkTheme" />
app:layout_constraintTop_toBottomOf="@id/tbDarkTheme" />
</androidx.constraintlayout.widget.ConstraintLayout>
</ScrollView>

View File

@@ -84,7 +84,7 @@
android:layout_marginStart="6dp"
android:text="error"
android:textAppearance="@style/TextAppearance.AppCompat.Small"
android:textStyle="bold"
android:textColor="?attr/colorWarning"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tvHost" />

View File

@@ -9,6 +9,7 @@
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_marginStart="6dp"
android:background="?attr/colorButtonNormal"
android:src="@drawable/baseline_edit_24"
app:layout_constraintBottom_toBottomOf="@+id/ivMessages"
app:layout_constraintStart_toStartOf="parent"
@@ -70,6 +71,16 @@
app:layout_constraintStart_toEndOf="@+id/ivState"
app:layout_constraintTop_toTopOf="@id/ivState" />
<ImageView
android:id="@+id/ivUnified"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_marginStart="6dp"
android:src="@drawable/baseline_folder_special_24"
app:layout_constraintBottom_toBottomOf="@id/ivState"
app:layout_constraintStart_toEndOf="@id/tvType"
app:layout_constraintTop_toTopOf="@id/ivState" />
<TextView
android:id="@+id/tvAfter"
android:layout_width="wrap_content"
@@ -98,7 +109,7 @@
android:layout_marginStart="6dp"
android:text="error"
android:textAppearance="@style/TextAppearance.AppCompat.Small"
android:textStyle="bold"
android:textColor="?attr/colorWarning"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/ivState" />

View File

@@ -96,7 +96,7 @@
android:layout_marginStart="6dp"
android:text="error"
android:textAppearance="@style/TextAppearance.AppCompat.Small"
android:textStyle="bold"
android:textColor="?attr/colorWarning"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tvHost" />

View File

@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/tvTime"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:ellipsize="start"
android:singleLine="true"
android:text="12:34:56"
android:textAppearance="@style/TextAppearance.AppCompat.Small"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@+id/ivEdit" />
<TextView
android:id="@+id/tvData"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="6dp"
android:text="log"
android:textAppearance="@android:style/TextAppearance.Small"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/tvTime"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -5,6 +5,17 @@
android:layout_height="wrap_content"
android:background="?attr/drawableItemBackground">
<ImageView
android:id="@+id/ivFlagged"
android:layout_width="21dp"
android:layout_height="21dp"
android:layout_marginStart="6dp"
android:src="@drawable/baseline_star_24"
android:visibility="visible"
app:layout_constraintBottom_toBottomOf="@id/tvFrom"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@+id/tvFrom" />
<TextView
android:id="@+id/tvFrom"
android:layout_width="0dp"
@@ -15,21 +26,10 @@
android:maxLines="1"
android:text="From"
android:textAppearance="@style/TextAppearance.AppCompat.Medium"
app:layout_constraintEnd_toStartOf="@+id/tvSize"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toStartOf="@+id/tvTime"
app:layout_constraintStart_toEndOf="@id/ivFlagged"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/tvSize"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="6dp"
android:maxLines="1"
android:text="123K"
android:textAppearance="@style/TextAppearance.AppCompat.Small"
app:layout_constraintBottom_toBottomOf="@id/tvFrom"
app:layout_constraintEnd_toStartOf="@+id/tvTime" />
<TextView
android:id="@+id/tvTime"
android:layout_width="wrap_content"
@@ -80,12 +80,22 @@
android:id="@+id/tvCount"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="6dp"
android:layout_marginEnd="3dp"
android:maxLines="1"
android:text="3"
android:textAppearance="@style/TextAppearance.AppCompat.Small"
app:layout_constraintBottom_toBottomOf="@id/tvSubject"
app:layout_constraintEnd_toEndOf="parent" />
app:layout_constraintEnd_toStartOf="@+id/ivThread" />
<ImageView
android:id="@+id/ivThread"
android:layout_width="15dp"
android:layout_height="15dp"
android:layout_marginEnd="6dp"
android:src="@drawable/baseline_message_24"
app:layout_constraintBottom_toBottomOf="@id/tvSubject"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@id/tvSubject" />
<TextView
android:id="@+id/tvError"

View File

@@ -3,9 +3,9 @@
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/action_trash"
android:id="@+id/action_delete"
android:icon="@drawable/baseline_delete_24"
android:title="@string/title_trash"
android:title="@string/title_delete"
app:showAsAction="ifRoom" />
<item

View File

@@ -3,9 +3,9 @@
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/action_trash"
android:id="@+id/action_delete"
android:icon="@drawable/baseline_delete_24"
android:title="@string/title_trash"
android:title="@string/title_delete"
app:showAsAction="ifRoom" />
<item

View File

@@ -9,7 +9,7 @@
app:showAsAction="ifRoom" />
<item
android:id="@+id/action_trash"
android:id="@+id/action_delete"
android:icon="@drawable/baseline_delete_24"
android:title="@string/title_trash"
app:showAsAction="ifRoom" />

View File

@@ -13,9 +13,4 @@
android:icon="@drawable/baseline_people_24"
android:title="@string/title_show_addresses"
app:showAsAction="always" />
<item
android:id="@+id/menu_encrypt"
android:title="@string/title_encrypt"
app:showAsAction="never" />
</menu>

View File

@@ -12,13 +12,7 @@
android:id="@+id/menu_thread"
android:icon="@drawable/baseline_message_24"
android:title="@string/title_thread"
app:showAsAction="never" />
<item
android:id="@+id/menu_seen"
android:icon="@drawable/baseline_visibility_off_24"
android:title="@string/title_seen"
app:showAsAction="never" />
app:showAsAction="always" />
<item
android:id="@+id/menu_forward"
@@ -32,14 +26,22 @@
android:title="@string/title_reply_all"
app:showAsAction="never" />
<item
android:id="@+id/menu_show_headers"
android:checkable="true"
android:icon="@drawable/baseline_visibility_24"
android:title="@string/title_show_headers"
app:showAsAction="never" />
<item
android:id="@+id/menu_show_html"
android:icon="@drawable/baseline_visibility_24"
android:title="@string/title_show_html"
app:showAsAction="never" />
<item
android:id="@+id/menu_answer"
android:icon="@drawable/baseline_reply_24"
android:title="@string/title_answer_reply"
app:showAsAction="never" />
<item
android:id="@+id/menu_decrypt"
android:title="@string/title_decrypt"
app:showAsAction="never" />
</menu>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.0 KiB

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.0 KiB

After

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.1 KiB

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.5 KiB

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

View File

@@ -26,10 +26,11 @@
<string name="menu_answers">Standard replies</string>
<string name="menu_operations">Operations</string>
<string name="menu_legend">Legend</string>
<string name="menu_faq">FAQ</string>
<string name="menu_faq">FAQ/support</string>
<string name="menu_pro">Pro features</string>
<string name="menu_privacy">Privacy</string>
<string name="menu_about">About</string>
<string name="menu_rate">Rate this app</string>
<string name="menu_other">Other apps</string>
<string name="title_eula">End-user license agreement</string>
<string name="title_agree">I agree</string>
@@ -45,10 +46,15 @@
<string name="title_setup_account_remark">To receive email</string>
<string name="title_setup_identity">Manage identities</string>
<string name="title_setup_identity_remark">To send email</string>
<string name="title_setup_doze">Disable battery optimizations</string>
<string name="title_setup_doze_remark">To continuously receive email (optional)</string>
<string name="title_setup_doze_instructions">In the next dialog, select \"All apps\" at the top, select this app and select and confirm \"Don\'t optimize\"</string>
<string name="title_setup_data">Disable data saving</string>
<string name="title_setup_permissions">Grant permissions</string>
<string name="title_setup_permissions_remark">To autocomplete addresses (optional)</string>
<string name="title_setup_to_do">To do</string>
<string name="title_setup_done">Done</string>
<string name="title_setup_light_theme">Light theme</string>
<string name="title_setup_dark_theme">Dark theme</string>
<string name="title_advanced">Advanced options</string>
<string name="title_advanced_webview">Use WebView to show external links</string>
@@ -64,6 +70,7 @@
<string name="title_account_linked">Linked account</string>
<string name="title_account_name">Account name</string>
<string name="title_account_name_hint">Used to differentiate folders</string>
<string name="title_account_signature">Signature text</string>
<string name="title_imap">IMAP</string>
<string name="title_smtp">SMTP</string>
<string name="title_provider">Provider</string>
@@ -74,9 +81,9 @@
<string name="title_user">User name</string>
<string name="title_password">Password</string>
<string name="title_authorize">Select account</string>
<string name="title_instructions">Instructions</string>
<string name="title_authorizing">Authorizing account &#8230;</string>
<string name="title_setup_advanced">Advanced</string>
<string name="title_store_sent">Store sent messages (enable if needed only)</string>
<string name="title_interval">Poll/keep-alive interval (minutes)</string>
<string name="title_synchronize_account">Synchronize (receive messages)</string>
<string name="title_synchronize_identity">Synchronize (send messages)</string>
<string name="title_primary_account">Primary (default account)</string>
@@ -90,11 +97,14 @@
<string name="title_no_user">User name missing</string>
<string name="title_no_password">Password missing</string>
<string name="title_no_drafts">Drafts folder missing</string>
<string name="title_no_idle">IDLE not supported</string>
<string name="title_no_uidplus">UIDPLUS not supported</string>
<string name="title_no_idle">IMAP IDLE not supported</string>
<string name="title_no_uidplus">IMAP UIDPLUS not supported</string>
<string name="title_account_delete">Delete this account permanently?</string>
<string name="title_identity_delete">Delete this identity permanently?</string>
<string name="title_pop">POP is not supported</string>
<string name="title_insecure">Insecure connections are not supported</string>
<string name="title_synchronize_folder">Synchronize (receive messages)</string>
<string name="title_unified_folder">Show in unified inbox</string>
<string name="title_after">Synchronize (days)</string>
<string name="title_folder_unified">Unified inbox</string>
<string name="title_folder_inbox">Inbox</string>
@@ -106,19 +116,24 @@
<string name="title_folder_sent">Sent</string>
<string name="title_folder_user">User</string>
<string name="title_folder_primary">Folders primary account</string>
<string name="title_folder_thread">Message thread</string>
<string name="title_folder_thread">Conversation</string>
<string name="title_no_messages">No messages</string>
<string name="title_link">link</string>
<string name="title_image">image</string>
<string name="title_show_images">Show images</string>
<string name="title_subject_reply">Re: %1$s</string>
<string name="title_subject_forward">Fwd: %1$s</string>
<string name="title_thread">Show thread</string>
<string name="title_thread">Show conversation</string>
<string name="title_seen">Mark read</string>
<string name="title_unseen">Mark unread</string>
<string name="title_flag">Add star</string>
<string name="title_unflag">Remove star</string>
<string name="title_forward">Forward</string>
<string name="title_reply_all">Reply to all</string>
<string name="title_show_headers">Show headers</string>
<string name="title_show_html">Show original</string>
<string name="title_trash">Trash</string>
<string name="title_delete">Delete</string>
<string name="title_spam">Spam</string>
<string name="title_move">Move</string>
<string name="title_archive">Archive</string>
@@ -140,33 +155,34 @@
<string name="title_send">Send</string>
<string name="title_show_addresses">Show CC/BCC</string>
<string name="title_add_attachment">Add attachment</string>
<string name="title_no_openpgp">OpenPGP not available</string>
<string name="title_not_encrypted">Encrypted message not found</string>
<string name="title_encrypt">Encrypt</string>
<string name="title_decrypt">Decrypt</string>
<string name="title_from_missing">Sender missing</string>
<string name="title_to_missing">Recipient missing</string>
<string name="title_attachments_missing">Attachments still loading</string>
<string name="title_draft_trashed">Draft trashed</string>
<string name="title_draft_deleted">Draft deleted</string>
<string name="title_draft_saved">Draft saved</string>
<string name="title_queued">Sending message</string>
<string name="title_search">Search</string>
<string name="title_search_hint">Search on server</string>
<string name="title_search_hint">Search sender/subject/text</string>
<string name="title_searching">Searching \'%1$s\'</string>
<string name="title_answer_reply">Standard reply</string>
<string name="title_answer_name">Answer name</string>
<string name="title_answer_text">Answer text</string>
<string name="title_legend_cc">CC/BCC</string>
<string name="title_legend_attachment">Attachment</string>
<string name="title_legend_thread">Conversation</string>
<string name="title_legend_synchronize">Synchronize</string>
<string name="title_legend_primary">Primary</string>
<string name="title_legend_unified">Unified inbox</string>
<string name="title_legend_disconnected">Disconnected</string>
<string name="title_legend_connecting">Connecting</string>
<string name="title_legend_connected">Connected</string>
<string name="title_legend_synchronizing">Synchronizing</string>
<string name="title_legend_closing">Closing</string>
<string name="title_hint_swipe">Swipe left to trash and swipe right to archive (if available)</string>
<string name="title_hint_actions">Swipe left to trash; swipe right to archive (if available); long press to mark read/unread</string>
<string name="title_understood">Understood</string>
<string name="title_issue">Do you have a question or problem?</string>
<string name="title_yes">Yes</string>
<string name="title_no">No</string>
<string name="title_pro_feature">This is a pro feature</string>
<string name="title_pro_list">List of pro features</string>
<string name="title_pro_purchase">Buy</string>
@@ -174,6 +190,7 @@
<string name="title_pro_activated">All pro features are activated</string>
<string name="title_pro_valid">All pro features activated</string>
<string name="title_pro_invalid">Invalid response</string>
<string name="title_log">Log</string>
<string name="title_debug_info">Debug info</string>
<string name="title_debug_info_remark">Please describe the problem and indicate the time of the problem:</string>
</resources>

View File

@@ -42,10 +42,11 @@
<string name="menu_answers">Standard replies</string>
<string name="menu_operations">Operations</string>
<string name="menu_legend">Legend</string>
<string name="menu_faq">FAQ</string>
<string name="menu_faq">FAQ/support</string>
<string name="menu_pro">Pro features</string>
<string name="menu_privacy">Privacy</string>
<string name="menu_about">About</string>
<string name="menu_rate">Rate this app</string>
<string name="menu_other">Other apps</string>
<string name="title_eula">End-user license agreement</string>
<string name="title_agree">I agree</string>
@@ -61,10 +62,15 @@
<string name="title_setup_account_remark">To receive email</string>
<string name="title_setup_identity">Manage identities</string>
<string name="title_setup_identity_remark">To send email</string>
<string name="title_setup_doze">Disable battery optimizations</string>
<string name="title_setup_doze_remark">To continuously receive email (optional)</string>
<string name="title_setup_doze_instructions">In the next dialog, select \"All apps\" at the top, select this app and select and confirm \"Don\'t optimize\"</string>
<string name="title_setup_data">Disable data saving</string>
<string name="title_setup_permissions">Grant permissions</string>
<string name="title_setup_permissions_remark">To autocomplete addresses (optional)</string>
<string name="title_setup_to_do">To do</string>
<string name="title_setup_done">Done</string>
<string name="title_setup_light_theme">Light theme</string>
<string name="title_setup_dark_theme">Dark theme</string>
<string name="title_advanced">Advanced options</string>
<string name="title_advanced_webview">Use WebView to show external links</string>
@@ -80,6 +86,7 @@
<string name="title_account_linked">Linked account</string>
<string name="title_account_name">Account name</string>
<string name="title_account_name_hint">Used to differentiate folders</string>
<string name="title_account_signature">Signature text</string>
<string name="title_imap">IMAP</string>
<string name="title_smtp">SMTP</string>
<string name="title_provider">Provider</string>
@@ -90,9 +97,9 @@
<string name="title_user">User name</string>
<string name="title_password">Password</string>
<string name="title_authorize">Select account</string>
<string name="title_instructions">Instructions</string>
<string name="title_authorizing">Authorizing account &#8230;</string>
<string name="title_setup_advanced">Advanced</string>
<string name="title_store_sent">Store sent messages (enable if needed only)</string>
<string name="title_interval">Poll/keep-alive interval (minutes)</string>
<string name="title_synchronize_account">Synchronize (receive messages)</string>
<string name="title_synchronize_identity">Synchronize (send messages)</string>
<string name="title_primary_account">Primary (default account)</string>
@@ -106,11 +113,14 @@
<string name="title_no_user">User name missing</string>
<string name="title_no_password">Password missing</string>
<string name="title_no_drafts">Drafts folder missing</string>
<string name="title_no_idle">IDLE not supported</string>
<string name="title_no_uidplus">UIDPLUS not supported</string>
<string name="title_no_idle">IMAP IDLE not supported</string>
<string name="title_no_uidplus">IMAP UIDPLUS not supported</string>
<string name="title_account_delete">Delete this account permanently?</string>
<string name="title_identity_delete">Delete this identity permanently?</string>
<string name="title_pop">POP is not supported</string>
<string name="title_insecure">Insecure connections are not supported</string>
<string name="title_synchronize_folder">Synchronize (receive messages)</string>
<string name="title_unified_folder">Show in unified inbox</string>
<string name="title_after">Synchronize (days)</string>
<string name="title_folder_unified">Unified inbox</string>
<string name="title_folder_inbox">Inbox</string>
@@ -122,19 +132,24 @@
<string name="title_folder_sent">Sent</string>
<string name="title_folder_user">User</string>
<string name="title_folder_primary">Folders primary account</string>
<string name="title_folder_thread">Message thread</string>
<string name="title_folder_thread">Conversation</string>
<string name="title_no_messages">No messages</string>
<string name="title_link">link</string>
<string name="title_image">image</string>
<string name="title_show_images">Show images</string>
<string name="title_subject_reply">Re: %1$s</string>
<string name="title_subject_forward">Fwd: %1$s</string>
<string name="title_thread">Show thread</string>
<string name="title_thread">Show conversation</string>
<string name="title_seen">Mark read</string>
<string name="title_unseen">Mark unread</string>
<string name="title_flag">Add star</string>
<string name="title_unflag">Remove star</string>
<string name="title_forward">Forward</string>
<string name="title_reply_all">Reply to all</string>
<string name="title_show_headers">Show headers</string>
<string name="title_show_html">Show original</string>
<string name="title_trash">Trash</string>
<string name="title_delete">Delete</string>
<string name="title_spam">Spam</string>
<string name="title_move">Move</string>
<string name="title_archive">Archive</string>
@@ -156,33 +171,34 @@
<string name="title_send">Send</string>
<string name="title_show_addresses">Show CC/BCC</string>
<string name="title_add_attachment">Add attachment</string>
<string name="title_no_openpgp">OpenPGP not available</string>
<string name="title_not_encrypted">Encrypted message not found</string>
<string name="title_encrypt">Encrypt</string>
<string name="title_decrypt">Decrypt</string>
<string name="title_from_missing">Sender missing</string>
<string name="title_to_missing">Recipient missing</string>
<string name="title_attachments_missing">Attachments still loading</string>
<string name="title_draft_trashed">Draft trashed</string>
<string name="title_draft_deleted">Draft deleted</string>
<string name="title_draft_saved">Draft saved</string>
<string name="title_queued">Sending message</string>
<string name="title_search">Search</string>
<string name="title_search_hint">Search on server</string>
<string name="title_search_hint">Search sender/subject/text</string>
<string name="title_searching">Searching \'%1$s\'</string>
<string name="title_answer_reply">Standard reply</string>
<string name="title_answer_name">Answer name</string>
<string name="title_answer_text">Answer text</string>
<string name="title_legend_cc">CC/BCC</string>
<string name="title_legend_attachment">Attachment</string>
<string name="title_legend_thread">Conversation</string>
<string name="title_legend_synchronize">Synchronize</string>
<string name="title_legend_primary">Primary</string>
<string name="title_legend_unified">Unified inbox</string>
<string name="title_legend_disconnected">Disconnected</string>
<string name="title_legend_connecting">Connecting</string>
<string name="title_legend_connected">Connected</string>
<string name="title_legend_synchronizing">Synchronizing</string>
<string name="title_legend_closing">Closing</string>
<string name="title_hint_swipe">Swipe left to trash and swipe right to archive (if available)</string>
<string name="title_hint_actions">Swipe left to trash; swipe right to archive (if available); long press to mark read/unread</string>
<string name="title_understood">Understood</string>
<string name="title_issue">Do you have a question or problem?</string>
<string name="title_yes">Yes</string>
<string name="title_no">No</string>
<string name="title_pro_feature">This is a pro feature</string>
<string name="title_pro_list">List of pro features</string>
<string name="title_pro_purchase">Buy</string>
@@ -190,6 +206,7 @@
<string name="title_pro_activated">All pro features are activated</string>
<string name="title_pro_valid">All pro features activated</string>
<string name="title_pro_invalid">Invalid response</string>
<string name="title_log">Log</string>
<string name="title_debug_info">Debug info</string>
<string name="title_debug_info_remark">Please describe the problem and indicate the time of the problem:</string>
</resources>

View File

@@ -42,10 +42,11 @@
<string name="menu_answers">Standard replies</string>
<string name="menu_operations">Operations</string>
<string name="menu_legend">Legend</string>
<string name="menu_faq">FAQ</string>
<string name="menu_faq">FAQ/support</string>
<string name="menu_pro">Pro features</string>
<string name="menu_privacy">Privacy</string>
<string name="menu_about">About</string>
<string name="menu_rate">Rate this app</string>
<string name="menu_other">Other apps</string>
<string name="title_eula">End-user license agreement</string>
<string name="title_agree">I agree</string>
@@ -61,10 +62,15 @@
<string name="title_setup_account_remark">To receive email</string>
<string name="title_setup_identity">Manage identities</string>
<string name="title_setup_identity_remark">To send email</string>
<string name="title_setup_doze">Disable battery optimizations</string>
<string name="title_setup_doze_remark">To continuously receive email (optional)</string>
<string name="title_setup_doze_instructions">In the next dialog, select \"All apps\" at the top, select this app and select and confirm \"Don\'t optimize\"</string>
<string name="title_setup_data">Disable data saving</string>
<string name="title_setup_permissions">Grant permissions</string>
<string name="title_setup_permissions_remark">To autocomplete addresses (optional)</string>
<string name="title_setup_to_do">To do</string>
<string name="title_setup_done">Done</string>
<string name="title_setup_light_theme">Light theme</string>
<string name="title_setup_dark_theme">Dark theme</string>
<string name="title_advanced">Advanced options</string>
<string name="title_advanced_webview">Use WebView to show external links</string>
@@ -80,6 +86,7 @@
<string name="title_account_linked">Linked account</string>
<string name="title_account_name">Account name</string>
<string name="title_account_name_hint">Used to differentiate folders</string>
<string name="title_account_signature">Signature text</string>
<string name="title_imap">IMAP</string>
<string name="title_smtp">SMTP</string>
<string name="title_provider">Provider</string>
@@ -90,9 +97,9 @@
<string name="title_user">User name</string>
<string name="title_password">Password</string>
<string name="title_authorize">Select account</string>
<string name="title_instructions">Instructions</string>
<string name="title_authorizing">Authorizing account &#8230;</string>
<string name="title_setup_advanced">Advanced</string>
<string name="title_store_sent">Store sent messages (enable if needed only)</string>
<string name="title_interval">Poll/keep-alive interval (minutes)</string>
<string name="title_synchronize_account">Synchronize (receive messages)</string>
<string name="title_synchronize_identity">Synchronize (send messages)</string>
<string name="title_primary_account">Primary (default account)</string>
@@ -106,11 +113,14 @@
<string name="title_no_user">User name missing</string>
<string name="title_no_password">Password missing</string>
<string name="title_no_drafts">Drafts folder missing</string>
<string name="title_no_idle">IDLE not supported</string>
<string name="title_no_uidplus">UIDPLUS not supported</string>
<string name="title_no_idle">IMAP IDLE not supported</string>
<string name="title_no_uidplus">IMAP UIDPLUS not supported</string>
<string name="title_account_delete">Delete this account permanently?</string>
<string name="title_identity_delete">Delete this identity permanently?</string>
<string name="title_pop">POP is not supported</string>
<string name="title_insecure">Insecure connections are not supported</string>
<string name="title_synchronize_folder">Synchronize (receive messages)</string>
<string name="title_unified_folder">Show in unified inbox</string>
<string name="title_after">Synchronize (days)</string>
<string name="title_folder_unified">Unified inbox</string>
<string name="title_folder_inbox">Inbox</string>
@@ -122,19 +132,24 @@
<string name="title_folder_sent">Sent</string>
<string name="title_folder_user">User</string>
<string name="title_folder_primary">Folders primary account</string>
<string name="title_folder_thread">Message thread</string>
<string name="title_folder_thread">Conversation</string>
<string name="title_no_messages">No messages</string>
<string name="title_link">link</string>
<string name="title_image">image</string>
<string name="title_show_images">Show images</string>
<string name="title_subject_reply">Re: %1$s</string>
<string name="title_subject_forward">Fwd: %1$s</string>
<string name="title_thread">Show thread</string>
<string name="title_thread">Show conversation</string>
<string name="title_seen">Mark read</string>
<string name="title_unseen">Mark unread</string>
<string name="title_flag">Add star</string>
<string name="title_unflag">Remove star</string>
<string name="title_forward">Forward</string>
<string name="title_reply_all">Reply to all</string>
<string name="title_show_headers">Show headers</string>
<string name="title_show_html">Show original</string>
<string name="title_trash">Trash</string>
<string name="title_delete">Delete</string>
<string name="title_spam">Spam</string>
<string name="title_move">Move</string>
<string name="title_archive">Archive</string>
@@ -156,33 +171,34 @@
<string name="title_send">Send</string>
<string name="title_show_addresses">Show CC/BCC</string>
<string name="title_add_attachment">Add attachment</string>
<string name="title_no_openpgp">OpenPGP not available</string>
<string name="title_not_encrypted">Encrypted message not found</string>
<string name="title_encrypt">Encrypt</string>
<string name="title_decrypt">Decrypt</string>
<string name="title_from_missing">Sender missing</string>
<string name="title_to_missing">Recipient missing</string>
<string name="title_attachments_missing">Attachments still loading</string>
<string name="title_draft_trashed">Draft trashed</string>
<string name="title_draft_deleted">Draft deleted</string>
<string name="title_draft_saved">Draft saved</string>
<string name="title_queued">Sending message</string>
<string name="title_search">Search</string>
<string name="title_search_hint">Search on server</string>
<string name="title_search_hint">Search sender/subject/text</string>
<string name="title_searching">Searching \'%1$s\'</string>
<string name="title_answer_reply">Standard reply</string>
<string name="title_answer_name">Answer name</string>
<string name="title_answer_text">Answer text</string>
<string name="title_legend_cc">CC/BCC</string>
<string name="title_legend_attachment">Attachment</string>
<string name="title_legend_thread">Conversation</string>
<string name="title_legend_synchronize">Synchronize</string>
<string name="title_legend_primary">Primary</string>
<string name="title_legend_unified">Unified inbox</string>
<string name="title_legend_disconnected">Disconnected</string>
<string name="title_legend_connecting">Connecting</string>
<string name="title_legend_connected">Connected</string>
<string name="title_legend_synchronizing">Synchronizing</string>
<string name="title_legend_closing">Closing</string>
<string name="title_hint_swipe">Swipe left to trash and swipe right to archive (if available)</string>
<string name="title_hint_actions">Swipe left to trash; swipe right to archive (if available); long press to mark read/unread</string>
<string name="title_understood">Understood</string>
<string name="title_issue">Do you have a question or problem?</string>
<string name="title_yes">Yes</string>
<string name="title_no">No</string>
<string name="title_pro_feature">This is a pro feature</string>
<string name="title_pro_list">List of pro features</string>
<string name="title_pro_purchase">Buy</string>
@@ -190,6 +206,7 @@
<string name="title_pro_activated">All pro features are activated</string>
<string name="title_pro_valid">All pro features activated</string>
<string name="title_pro_invalid">Invalid response</string>
<string name="title_log">Log</string>
<string name="title_debug_info">Debug info</string>
<string name="title_debug_info_remark">Please describe the problem and indicate the time of the problem:</string>
</resources>

View File

@@ -42,10 +42,11 @@
<string name="menu_answers">Standard replies</string>
<string name="menu_operations">Operations</string>
<string name="menu_legend">Legend</string>
<string name="menu_faq">FAQ</string>
<string name="menu_faq">FAQ/support</string>
<string name="menu_pro">Pro features</string>
<string name="menu_privacy">Privacy</string>
<string name="menu_about">About</string>
<string name="menu_rate">Rate this app</string>
<string name="menu_other">Other apps</string>
<string name="title_eula">End-user license agreement</string>
<string name="title_agree">I agree</string>
@@ -61,10 +62,15 @@
<string name="title_setup_account_remark">To receive email</string>
<string name="title_setup_identity">Manage identities</string>
<string name="title_setup_identity_remark">To send email</string>
<string name="title_setup_doze">Disable battery optimizations</string>
<string name="title_setup_doze_remark">To continuously receive email (optional)</string>
<string name="title_setup_doze_instructions">In the next dialog, select \"All apps\" at the top, select this app and select and confirm \"Don\'t optimize\"</string>
<string name="title_setup_data">Disable data saving</string>
<string name="title_setup_permissions">Grant permissions</string>
<string name="title_setup_permissions_remark">To autocomplete addresses (optional)</string>
<string name="title_setup_to_do">To do</string>
<string name="title_setup_done">Done</string>
<string name="title_setup_light_theme">Light theme</string>
<string name="title_setup_dark_theme">Dark theme</string>
<string name="title_advanced">Advanced options</string>
<string name="title_advanced_webview">Use WebView to show external links</string>
@@ -80,6 +86,7 @@
<string name="title_account_linked">Linked account</string>
<string name="title_account_name">Account name</string>
<string name="title_account_name_hint">Used to differentiate folders</string>
<string name="title_account_signature">Signature text</string>
<string name="title_imap">IMAP</string>
<string name="title_smtp">SMTP</string>
<string name="title_provider">Provider</string>
@@ -90,9 +97,9 @@
<string name="title_user">User name</string>
<string name="title_password">Password</string>
<string name="title_authorize">Select account</string>
<string name="title_instructions">Instructions</string>
<string name="title_authorizing">Authorizing account &#8230;</string>
<string name="title_setup_advanced">Advanced</string>
<string name="title_store_sent">Store sent messages (enable if needed only)</string>
<string name="title_interval">Poll/keep-alive interval (minutes)</string>
<string name="title_synchronize_account">Synchronize (receive messages)</string>
<string name="title_synchronize_identity">Synchronize (send messages)</string>
<string name="title_primary_account">Primary (default account)</string>
@@ -106,11 +113,14 @@
<string name="title_no_user">User name missing</string>
<string name="title_no_password">Password missing</string>
<string name="title_no_drafts">Drafts folder missing</string>
<string name="title_no_idle">IDLE not supported</string>
<string name="title_no_uidplus">UIDPLUS not supported</string>
<string name="title_no_idle">IMAP IDLE not supported</string>
<string name="title_no_uidplus">IMAP UIDPLUS not supported</string>
<string name="title_account_delete">Delete this account permanently?</string>
<string name="title_identity_delete">Delete this identity permanently?</string>
<string name="title_pop">POP is not supported</string>
<string name="title_insecure">Insecure connections are not supported</string>
<string name="title_synchronize_folder">Synchronize (receive messages)</string>
<string name="title_unified_folder">Show in unified inbox</string>
<string name="title_after">Synchronize (days)</string>
<string name="title_folder_unified">Unified inbox</string>
<string name="title_folder_inbox">Inbox</string>
@@ -122,19 +132,24 @@
<string name="title_folder_sent">Sent</string>
<string name="title_folder_user">User</string>
<string name="title_folder_primary">Folders primary account</string>
<string name="title_folder_thread">Message thread</string>
<string name="title_folder_thread">Conversation</string>
<string name="title_no_messages">No messages</string>
<string name="title_link">link</string>
<string name="title_image">image</string>
<string name="title_show_images">Show images</string>
<string name="title_subject_reply">Re: %1$s</string>
<string name="title_subject_forward">Fwd: %1$s</string>
<string name="title_thread">Show thread</string>
<string name="title_thread">Show conversation</string>
<string name="title_seen">Mark read</string>
<string name="title_unseen">Mark unread</string>
<string name="title_flag">Add star</string>
<string name="title_unflag">Remove star</string>
<string name="title_forward">Forward</string>
<string name="title_reply_all">Reply to all</string>
<string name="title_show_headers">Show headers</string>
<string name="title_show_html">Show original</string>
<string name="title_trash">Trash</string>
<string name="title_delete">Delete</string>
<string name="title_spam">Spam</string>
<string name="title_move">Move</string>
<string name="title_archive">Archive</string>
@@ -156,33 +171,34 @@
<string name="title_send">Send</string>
<string name="title_show_addresses">Show CC/BCC</string>
<string name="title_add_attachment">Add attachment</string>
<string name="title_no_openpgp">OpenPGP not available</string>
<string name="title_not_encrypted">Encrypted message not found</string>
<string name="title_encrypt">Encrypt</string>
<string name="title_decrypt">Decrypt</string>
<string name="title_from_missing">Sender missing</string>
<string name="title_to_missing">Recipient missing</string>
<string name="title_attachments_missing">Attachments still loading</string>
<string name="title_draft_trashed">Draft trashed</string>
<string name="title_draft_deleted">Draft deleted</string>
<string name="title_draft_saved">Draft saved</string>
<string name="title_queued">Sending message</string>
<string name="title_search">Search</string>
<string name="title_search_hint">Search on server</string>
<string name="title_search_hint">Search sender/subject/text</string>
<string name="title_searching">Searching \'%1$s\'</string>
<string name="title_answer_reply">Standard reply</string>
<string name="title_answer_name">Answer name</string>
<string name="title_answer_text">Answer text</string>
<string name="title_legend_cc">CC/BCC</string>
<string name="title_legend_attachment">Attachment</string>
<string name="title_legend_thread">Conversation</string>
<string name="title_legend_synchronize">Synchronize</string>
<string name="title_legend_primary">Primary</string>
<string name="title_legend_unified">Unified inbox</string>
<string name="title_legend_disconnected">Disconnected</string>
<string name="title_legend_connecting">Connecting</string>
<string name="title_legend_connected">Connected</string>
<string name="title_legend_synchronizing">Synchronizing</string>
<string name="title_legend_closing">Closing</string>
<string name="title_hint_swipe">Swipe left to trash and swipe right to archive (if available)</string>
<string name="title_hint_actions">Swipe left to trash; swipe right to archive (if available); long press to mark read/unread</string>
<string name="title_understood">Understood</string>
<string name="title_issue">Do you have a question or problem?</string>
<string name="title_yes">Yes</string>
<string name="title_no">No</string>
<string name="title_pro_feature">This is a pro feature</string>
<string name="title_pro_list">List of pro features</string>
<string name="title_pro_purchase">Buy</string>
@@ -190,6 +206,7 @@
<string name="title_pro_activated">All pro features are activated</string>
<string name="title_pro_valid">All pro features activated</string>
<string name="title_pro_invalid">Invalid response</string>
<string name="title_log">Log</string>
<string name="title_debug_info">Debug info</string>
<string name="title_debug_info_remark">Please describe the problem and indicate the time of the problem:</string>
</resources>

View File

@@ -42,10 +42,11 @@
<string name="menu_answers">Standard replies</string>
<string name="menu_operations">Operations</string>
<string name="menu_legend">Legend</string>
<string name="menu_faq">FAQ</string>
<string name="menu_faq">FAQ/support</string>
<string name="menu_pro">Pro features</string>
<string name="menu_privacy">Privacy</string>
<string name="menu_about">About</string>
<string name="menu_rate">Rate this app</string>
<string name="menu_other">Other apps</string>
<string name="title_eula">End-user license agreement</string>
<string name="title_agree">I agree</string>
@@ -61,10 +62,15 @@
<string name="title_setup_account_remark">To receive email</string>
<string name="title_setup_identity">Manage identities</string>
<string name="title_setup_identity_remark">To send email</string>
<string name="title_setup_doze">Disable battery optimizations</string>
<string name="title_setup_doze_remark">To continuously receive email (optional)</string>
<string name="title_setup_doze_instructions">In the next dialog, select \"All apps\" at the top, select this app and select and confirm \"Don\'t optimize\"</string>
<string name="title_setup_data">Disable data saving</string>
<string name="title_setup_permissions">Grant permissions</string>
<string name="title_setup_permissions_remark">To autocomplete addresses (optional)</string>
<string name="title_setup_to_do">To do</string>
<string name="title_setup_done">Done</string>
<string name="title_setup_light_theme">Light theme</string>
<string name="title_setup_dark_theme">Dark theme</string>
<string name="title_advanced">Advanced options</string>
<string name="title_advanced_webview">Use WebView to show external links</string>
@@ -80,6 +86,7 @@
<string name="title_account_linked">Linked account</string>
<string name="title_account_name">Account name</string>
<string name="title_account_name_hint">Used to differentiate folders</string>
<string name="title_account_signature">Signature text</string>
<string name="title_imap">IMAP</string>
<string name="title_smtp">SMTP</string>
<string name="title_provider">Provider</string>
@@ -90,9 +97,9 @@
<string name="title_user">User name</string>
<string name="title_password">Password</string>
<string name="title_authorize">Select account</string>
<string name="title_instructions">Instructions</string>
<string name="title_authorizing">Authorizing account &#8230;</string>
<string name="title_setup_advanced">Advanced</string>
<string name="title_store_sent">Store sent messages (enable if needed only)</string>
<string name="title_interval">Poll/keep-alive interval (minutes)</string>
<string name="title_synchronize_account">Synchronize (receive messages)</string>
<string name="title_synchronize_identity">Synchronize (send messages)</string>
<string name="title_primary_account">Primary (default account)</string>
@@ -106,11 +113,14 @@
<string name="title_no_user">User name missing</string>
<string name="title_no_password">Password missing</string>
<string name="title_no_drafts">Drafts folder missing</string>
<string name="title_no_idle">IDLE not supported</string>
<string name="title_no_uidplus">UIDPLUS not supported</string>
<string name="title_no_idle">IMAP IDLE not supported</string>
<string name="title_no_uidplus">IMAP UIDPLUS not supported</string>
<string name="title_account_delete">Delete this account permanently?</string>
<string name="title_identity_delete">Delete this identity permanently?</string>
<string name="title_pop">POP is not supported</string>
<string name="title_insecure">Insecure connections are not supported</string>
<string name="title_synchronize_folder">Synchronize (receive messages)</string>
<string name="title_unified_folder">Show in unified inbox</string>
<string name="title_after">Synchronize (days)</string>
<string name="title_folder_unified">Unified inbox</string>
<string name="title_folder_inbox">Inbox</string>
@@ -122,19 +132,24 @@
<string name="title_folder_sent">Sent</string>
<string name="title_folder_user">User</string>
<string name="title_folder_primary">Folders primary account</string>
<string name="title_folder_thread">Message thread</string>
<string name="title_folder_thread">Conversation</string>
<string name="title_no_messages">No messages</string>
<string name="title_link">link</string>
<string name="title_image">image</string>
<string name="title_show_images">Show images</string>
<string name="title_subject_reply">Re: %1$s</string>
<string name="title_subject_forward">Fwd: %1$s</string>
<string name="title_thread">Show thread</string>
<string name="title_thread">Show conversation</string>
<string name="title_seen">Mark read</string>
<string name="title_unseen">Mark unread</string>
<string name="title_flag">Add star</string>
<string name="title_unflag">Remove star</string>
<string name="title_forward">Forward</string>
<string name="title_reply_all">Reply to all</string>
<string name="title_show_headers">Show headers</string>
<string name="title_show_html">Show original</string>
<string name="title_trash">Trash</string>
<string name="title_delete">Delete</string>
<string name="title_spam">Spam</string>
<string name="title_move">Move</string>
<string name="title_archive">Archive</string>
@@ -156,33 +171,34 @@
<string name="title_send">Send</string>
<string name="title_show_addresses">Show CC/BCC</string>
<string name="title_add_attachment">Add attachment</string>
<string name="title_no_openpgp">OpenPGP not available</string>
<string name="title_not_encrypted">Encrypted message not found</string>
<string name="title_encrypt">Encrypt</string>
<string name="title_decrypt">Decrypt</string>
<string name="title_from_missing">Sender missing</string>
<string name="title_to_missing">Recipient missing</string>
<string name="title_attachments_missing">Attachments still loading</string>
<string name="title_draft_trashed">Draft trashed</string>
<string name="title_draft_deleted">Draft deleted</string>
<string name="title_draft_saved">Draft saved</string>
<string name="title_queued">Sending message</string>
<string name="title_search">Search</string>
<string name="title_search_hint">Search on server</string>
<string name="title_search_hint">Search sender/subject/text</string>
<string name="title_searching">Searching \'%1$s\'</string>
<string name="title_answer_reply">Standard reply</string>
<string name="title_answer_name">Answer name</string>
<string name="title_answer_text">Answer text</string>
<string name="title_legend_cc">CC/BCC</string>
<string name="title_legend_attachment">Attachment</string>
<string name="title_legend_thread">Conversation</string>
<string name="title_legend_synchronize">Synchronize</string>
<string name="title_legend_primary">Primary</string>
<string name="title_legend_unified">Unified inbox</string>
<string name="title_legend_disconnected">Disconnected</string>
<string name="title_legend_connecting">Connecting</string>
<string name="title_legend_connected">Connected</string>
<string name="title_legend_synchronizing">Synchronizing</string>
<string name="title_legend_closing">Closing</string>
<string name="title_hint_swipe">Swipe left to trash and swipe right to archive (if available)</string>
<string name="title_hint_actions">Swipe left to trash; swipe right to archive (if available); long press to mark read/unread</string>
<string name="title_understood">Understood</string>
<string name="title_issue">Do you have a question or problem?</string>
<string name="title_yes">Yes</string>
<string name="title_no">No</string>
<string name="title_pro_feature">This is a pro feature</string>
<string name="title_pro_list">List of pro features</string>
<string name="title_pro_purchase">Buy</string>
@@ -190,6 +206,7 @@
<string name="title_pro_activated">All pro features are activated</string>
<string name="title_pro_valid">All pro features activated</string>
<string name="title_pro_invalid">Invalid response</string>
<string name="title_log">Log</string>
<string name="title_debug_info">Debug info</string>
<string name="title_debug_info_remark">Please describe the problem and indicate the time of the problem:</string>
</resources>

View File

@@ -42,10 +42,11 @@
<string name="menu_answers">Standard replies</string>
<string name="menu_operations">Operations</string>
<string name="menu_legend">Legend</string>
<string name="menu_faq">FAQ</string>
<string name="menu_faq">FAQ/support</string>
<string name="menu_pro">Pro features</string>
<string name="menu_privacy">Privacy</string>
<string name="menu_about">About</string>
<string name="menu_rate">Rate this app</string>
<string name="menu_other">Other apps</string>
<string name="title_eula">End-user license agreement</string>
<string name="title_agree">I agree</string>
@@ -61,10 +62,15 @@
<string name="title_setup_account_remark">To receive email</string>
<string name="title_setup_identity">Manage identities</string>
<string name="title_setup_identity_remark">To send email</string>
<string name="title_setup_doze">Disable battery optimizations</string>
<string name="title_setup_doze_remark">To continuously receive email (optional)</string>
<string name="title_setup_doze_instructions">In the next dialog, select \"All apps\" at the top, select this app and select and confirm \"Don\'t optimize\"</string>
<string name="title_setup_data">Disable data saving</string>
<string name="title_setup_permissions">Grant permissions</string>
<string name="title_setup_permissions_remark">To autocomplete addresses (optional)</string>
<string name="title_setup_to_do">To do</string>
<string name="title_setup_done">Done</string>
<string name="title_setup_light_theme">Light theme</string>
<string name="title_setup_dark_theme">Dark theme</string>
<string name="title_advanced">Advanced options</string>
<string name="title_advanced_webview">Use WebView to show external links</string>
@@ -80,6 +86,7 @@
<string name="title_account_linked">Linked account</string>
<string name="title_account_name">Account name</string>
<string name="title_account_name_hint">Used to differentiate folders</string>
<string name="title_account_signature">Signature text</string>
<string name="title_imap">IMAP</string>
<string name="title_smtp">SMTP</string>
<string name="title_provider">Provider</string>
@@ -90,9 +97,9 @@
<string name="title_user">User name</string>
<string name="title_password">Password</string>
<string name="title_authorize">Select account</string>
<string name="title_instructions">Instructions</string>
<string name="title_authorizing">Authorizing account &#8230;</string>
<string name="title_setup_advanced">Advanced</string>
<string name="title_store_sent">Store sent messages (enable if needed only)</string>
<string name="title_interval">Poll/keep-alive interval (minutes)</string>
<string name="title_synchronize_account">Synchronize (receive messages)</string>
<string name="title_synchronize_identity">Synchronize (send messages)</string>
<string name="title_primary_account">Primary (default account)</string>
@@ -106,11 +113,14 @@
<string name="title_no_user">User name missing</string>
<string name="title_no_password">Password missing</string>
<string name="title_no_drafts">Drafts folder missing</string>
<string name="title_no_idle">IDLE not supported</string>
<string name="title_no_uidplus">UIDPLUS not supported</string>
<string name="title_no_idle">IMAP IDLE not supported</string>
<string name="title_no_uidplus">IMAP UIDPLUS not supported</string>
<string name="title_account_delete">Delete this account permanently?</string>
<string name="title_identity_delete">Delete this identity permanently?</string>
<string name="title_pop">POP is not supported</string>
<string name="title_insecure">Insecure connections are not supported</string>
<string name="title_synchronize_folder">Synchronize (receive messages)</string>
<string name="title_unified_folder">Show in unified inbox</string>
<string name="title_after">Synchronize (days)</string>
<string name="title_folder_unified">Unified inbox</string>
<string name="title_folder_inbox">Inbox</string>
@@ -122,19 +132,24 @@
<string name="title_folder_sent">Sent</string>
<string name="title_folder_user">User</string>
<string name="title_folder_primary">Folders primary account</string>
<string name="title_folder_thread">Message thread</string>
<string name="title_folder_thread">Conversation</string>
<string name="title_no_messages">No messages</string>
<string name="title_link">link</string>
<string name="title_image">image</string>
<string name="title_show_images">Show images</string>
<string name="title_subject_reply">Re: %1$s</string>
<string name="title_subject_forward">Fwd: %1$s</string>
<string name="title_thread">Show thread</string>
<string name="title_thread">Show conversation</string>
<string name="title_seen">Mark read</string>
<string name="title_unseen">Mark unread</string>
<string name="title_flag">Add star</string>
<string name="title_unflag">Remove star</string>
<string name="title_forward">Forward</string>
<string name="title_reply_all">Reply to all</string>
<string name="title_show_headers">Show headers</string>
<string name="title_show_html">Show original</string>
<string name="title_trash">Trash</string>
<string name="title_delete">Delete</string>
<string name="title_spam">Spam</string>
<string name="title_move">Move</string>
<string name="title_archive">Archive</string>
@@ -156,33 +171,34 @@
<string name="title_send">Send</string>
<string name="title_show_addresses">Show CC/BCC</string>
<string name="title_add_attachment">Add attachment</string>
<string name="title_no_openpgp">OpenPGP not available</string>
<string name="title_not_encrypted">Encrypted message not found</string>
<string name="title_encrypt">Encrypt</string>
<string name="title_decrypt">Decrypt</string>
<string name="title_from_missing">Sender missing</string>
<string name="title_to_missing">Recipient missing</string>
<string name="title_attachments_missing">Attachments still loading</string>
<string name="title_draft_trashed">Draft trashed</string>
<string name="title_draft_deleted">Draft deleted</string>
<string name="title_draft_saved">Draft saved</string>
<string name="title_queued">Sending message</string>
<string name="title_search">Search</string>
<string name="title_search_hint">Search on server</string>
<string name="title_search_hint">Search sender/subject/text</string>
<string name="title_searching">Searching \'%1$s\'</string>
<string name="title_answer_reply">Standard reply</string>
<string name="title_answer_name">Answer name</string>
<string name="title_answer_text">Answer text</string>
<string name="title_legend_cc">CC/BCC</string>
<string name="title_legend_attachment">Attachment</string>
<string name="title_legend_thread">Conversation</string>
<string name="title_legend_synchronize">Synchronize</string>
<string name="title_legend_primary">Primary</string>
<string name="title_legend_unified">Unified inbox</string>
<string name="title_legend_disconnected">Disconnected</string>
<string name="title_legend_connecting">Connecting</string>
<string name="title_legend_connected">Connected</string>
<string name="title_legend_synchronizing">Synchronizing</string>
<string name="title_legend_closing">Closing</string>
<string name="title_hint_swipe">Swipe left to trash and swipe right to archive (if available)</string>
<string name="title_hint_actions">Swipe left to trash; swipe right to archive (if available); long press to mark read/unread</string>
<string name="title_understood">Understood</string>
<string name="title_issue">Do you have a question or problem?</string>
<string name="title_yes">Yes</string>
<string name="title_no">No</string>
<string name="title_pro_feature">This is a pro feature</string>
<string name="title_pro_list">List of pro features</string>
<string name="title_pro_purchase">Buy</string>
@@ -190,6 +206,7 @@
<string name="title_pro_activated">All pro features are activated</string>
<string name="title_pro_valid">All pro features activated</string>
<string name="title_pro_invalid">Invalid response</string>
<string name="title_log">Log</string>
<string name="title_debug_info">Debug info</string>
<string name="title_debug_info_remark">Please describe the problem and indicate the time of the problem:</string>
</resources>

View File

@@ -26,10 +26,11 @@
<string name="menu_answers">Standard replies</string>
<string name="menu_operations">Operations</string>
<string name="menu_legend">Legend</string>
<string name="menu_faq">FAQ</string>
<string name="menu_faq">FAQ/support</string>
<string name="menu_pro">Pro features</string>
<string name="menu_privacy">Privacy</string>
<string name="menu_about">About</string>
<string name="menu_rate">Rate this app</string>
<string name="menu_other">Other apps</string>
<string name="title_eula">End-user license agreement</string>
<string name="title_agree">I agree</string>
@@ -45,10 +46,15 @@
<string name="title_setup_account_remark">To receive email</string>
<string name="title_setup_identity">Manage identities</string>
<string name="title_setup_identity_remark">To send email</string>
<string name="title_setup_doze">Disable battery optimizations</string>
<string name="title_setup_doze_remark">To continuously receive email (optional)</string>
<string name="title_setup_doze_instructions">In the next dialog, select \"All apps\" at the top, select this app and select and confirm \"Don\'t optimize\"</string>
<string name="title_setup_data">Disable data saving</string>
<string name="title_setup_permissions">Grant permissions</string>
<string name="title_setup_permissions_remark">To autocomplete addresses (optional)</string>
<string name="title_setup_to_do">To do</string>
<string name="title_setup_done">Done</string>
<string name="title_setup_light_theme">Light theme</string>
<string name="title_setup_dark_theme">Dark theme</string>
<string name="title_advanced">Advanced options</string>
<string name="title_advanced_webview">Use WebView to show external links</string>
@@ -64,6 +70,7 @@
<string name="title_account_linked">Linked account</string>
<string name="title_account_name">Account name</string>
<string name="title_account_name_hint">Used to differentiate folders</string>
<string name="title_account_signature">Signature text</string>
<string name="title_imap">IMAP</string>
<string name="title_smtp">SMTP</string>
<string name="title_provider">Provider</string>
@@ -74,9 +81,9 @@
<string name="title_user">User name</string>
<string name="title_password">Password</string>
<string name="title_authorize">Select account</string>
<string name="title_instructions">Instructions</string>
<string name="title_authorizing">Authorizing account &#8230;</string>
<string name="title_setup_advanced">Advanced</string>
<string name="title_store_sent">Store sent messages (enable if needed only)</string>
<string name="title_interval">Poll/keep-alive interval (minutes)</string>
<string name="title_synchronize_account">Synchronize (receive messages)</string>
<string name="title_synchronize_identity">Synchronize (send messages)</string>
<string name="title_primary_account">Primary (default account)</string>
@@ -90,11 +97,14 @@
<string name="title_no_user">User name missing</string>
<string name="title_no_password">Password missing</string>
<string name="title_no_drafts">Drafts folder missing</string>
<string name="title_no_idle">IDLE not supported</string>
<string name="title_no_uidplus">UIDPLUS not supported</string>
<string name="title_no_idle">IMAP IDLE not supported</string>
<string name="title_no_uidplus">IMAP UIDPLUS not supported</string>
<string name="title_account_delete">Delete this account permanently?</string>
<string name="title_identity_delete">Delete this identity permanently?</string>
<string name="title_pop">POP is not supported</string>
<string name="title_insecure">Insecure connections are not supported</string>
<string name="title_synchronize_folder">Synchronize (receive messages)</string>
<string name="title_unified_folder">Show in unified inbox</string>
<string name="title_after">Synchronize (days)</string>
<string name="title_folder_unified">Unified inbox</string>
<string name="title_folder_inbox">Inbox</string>
@@ -106,19 +116,24 @@
<string name="title_folder_sent">Sent</string>
<string name="title_folder_user">User</string>
<string name="title_folder_primary">Folders primary account</string>
<string name="title_folder_thread">Message thread</string>
<string name="title_folder_thread">Conversation</string>
<string name="title_no_messages">No messages</string>
<string name="title_link">link</string>
<string name="title_image">image</string>
<string name="title_show_images">Show images</string>
<string name="title_subject_reply">Re: %1$s</string>
<string name="title_subject_forward">Fwd: %1$s</string>
<string name="title_thread">Show thread</string>
<string name="title_thread">Show conversation</string>
<string name="title_seen">Mark read</string>
<string name="title_unseen">Mark unread</string>
<string name="title_flag">Add star</string>
<string name="title_unflag">Remove star</string>
<string name="title_forward">Forward</string>
<string name="title_reply_all">Reply to all</string>
<string name="title_show_headers">Show headers</string>
<string name="title_show_html">Show original</string>
<string name="title_trash">Trash</string>
<string name="title_delete">Delete</string>
<string name="title_spam">Spam</string>
<string name="title_move">Move</string>
<string name="title_archive">Archive</string>
@@ -140,33 +155,34 @@
<string name="title_send">Send</string>
<string name="title_show_addresses">Show CC/BCC</string>
<string name="title_add_attachment">Add attachment</string>
<string name="title_no_openpgp">OpenPGP not available</string>
<string name="title_not_encrypted">Encrypted message not found</string>
<string name="title_encrypt">Encrypt</string>
<string name="title_decrypt">Decrypt</string>
<string name="title_from_missing">Sender missing</string>
<string name="title_to_missing">Recipient missing</string>
<string name="title_attachments_missing">Attachments still loading</string>
<string name="title_draft_trashed">Draft trashed</string>
<string name="title_draft_deleted">Draft deleted</string>
<string name="title_draft_saved">Draft saved</string>
<string name="title_queued">Sending message</string>
<string name="title_search">Search</string>
<string name="title_search_hint">Search on server</string>
<string name="title_search_hint">Search sender/subject/text</string>
<string name="title_searching">Searching \'%1$s\'</string>
<string name="title_answer_reply">Standard reply</string>
<string name="title_answer_name">Answer name</string>
<string name="title_answer_text">Answer text</string>
<string name="title_legend_cc">CC/BCC</string>
<string name="title_legend_attachment">Attachment</string>
<string name="title_legend_thread">Conversation</string>
<string name="title_legend_synchronize">Synchronize</string>
<string name="title_legend_primary">Primary</string>
<string name="title_legend_unified">Unified inbox</string>
<string name="title_legend_disconnected">Disconnected</string>
<string name="title_legend_connecting">Connecting</string>
<string name="title_legend_connected">Connected</string>
<string name="title_legend_synchronizing">Synchronizing</string>
<string name="title_legend_closing">Closing</string>
<string name="title_hint_swipe">Swipe left to trash and swipe right to archive (if available)</string>
<string name="title_hint_actions">Swipe left to trash; swipe right to archive (if available); long press to mark read/unread</string>
<string name="title_understood">Understood</string>
<string name="title_issue">Do you have a question or problem?</string>
<string name="title_yes">Yes</string>
<string name="title_no">No</string>
<string name="title_pro_feature">This is a pro feature</string>
<string name="title_pro_list">List of pro features</string>
<string name="title_pro_purchase">Buy</string>
@@ -174,6 +190,7 @@
<string name="title_pro_activated">All pro features are activated</string>
<string name="title_pro_valid">All pro features activated</string>
<string name="title_pro_invalid">Invalid response</string>
<string name="title_log">Log</string>
<string name="title_debug_info">Debug info</string>
<string name="title_debug_info_remark">Please describe the problem and indicate the time of the problem:</string>
</resources>

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