Compare commits

...

52 Commits
0.2 ... 0.4

Author SHA1 Message Date
M66B
c8462c2845 0.4 release 2018-08-03 19:18:27 +00:00
M66B
70e0913331 Attachment download 2018-08-03 19:16:39 +00:00
M66B
bc9a26c2c7 Store crash info as draft email 2018-08-03 18:07:12 +00:00
M66B
7e89563b48 Fixed crash 2018-08-03 17:34:27 +00:00
M66B
4ada1dba5d Improved search for standard folders 2018-08-03 17:09:12 +00:00
M66B
cf73512897 Better folder sort 2018-08-03 16:54:54 +00:00
M66B
f31644894e Fixed bottom navigation enabled state 2018-08-03 16:38:35 +00:00
M66B
0a1759b198 Revert "Layout improvement"
This reverts commit 222a16b963.
2018-08-03 16:22:21 +00:00
M66B
5f77582bec Create outbox on setup
To make sure the event listener is started
2018-08-03 16:21:18 +00:00
M66B
222a16b963 Layout improvement 2018-08-03 16:00:06 +00:00
M66B
9f248a7b30 Show alt title in santized text 2018-08-03 15:33:57 +00:00
M66B
63e40513fb Several fixes 2018-08-03 15:12:35 +00:00
M66B
d32df01e25 Handle providers without drafts folder
for example free.fr
2018-08-03 14:41:31 +00:00
M66B
8ac235791f Move thread action to the right and show if available only 2018-08-03 14:01:08 +00:00
M66B
dabf802d84 Make bottom navigation actions invisible when unavailable 2018-08-03 13:50:55 +00:00
M66B
b130da7bc1 List attachments in message view 2018-08-03 13:46:25 +00:00
M66B
14efe62e91 Show if attachments in message list item 2018-08-03 12:27:49 +00:00
M66B
bb4bed926a Get attachment meta info 2018-08-03 12:08:22 +00:00
M66B
485ef3ff56 Allow viewing account/identity password 2018-08-03 10:56:11 +00:00
M66B
9fbc8f1900 Provider Yahoo! supports SSL without STARTTLS 2018-08-03 10:40:46 +00:00
M66B
58adbf64c3 Added inline viewing of images and links 2018-08-03 10:32:17 +00:00
M66B
74b6cb037d Refactoring 2018-08-03 09:58:44 +00:00
M66B
fb4f0f2f58 Small layout improvement 2018-08-03 09:27:44 +00:00
M66B
f67f822267 Localize folder type 2018-08-03 09:21:57 +00:00
M66B
20d60fafe8 Check drafts on saving primary account only 2018-08-03 09:05:05 +00:00
M66B
29ae761435 Small improvement 2018-08-03 08:43:25 +00:00
M66B
3d3bf4b4d3 Disable compose on no primary drafts folder 2018-08-03 08:42:56 +00:00
M66B
d17ff4f188 Updated text 2018-08-03 08:15:58 +00:00
M66B
b3742cd525 Enable labels on main message actions
Refs #5
2018-08-03 08:15:13 +00:00
M66B
5d4c13ad7f Add provider free.fr 2018-08-03 07:51:11 +00:00
M66B
774f9b3f36 Added cc/bcc 2018-08-03 07:39:43 +00:00
M66B
bcf86385ae Consume message removed exception on processing operations 2018-08-03 05:08:58 +00:00
M66B
4d6c41674d Added hint about long press folder 2018-08-03 05:02:27 +00:00
M66B
a499808691 Fixed typo 2018-08-03 05:01:11 +00:00
M66B
01375cc84e Layout fixes 2018-08-03 04:54:51 +00:00
M66B
30a2b5ee31 Sort providers on name 2018-08-03 04:34:02 +00:00
M66B
a78fb6ba91 Added providers Posteo.de and Yahoo! 2018-08-03 04:33:37 +00:00
M66B
ffb85c5403 Updated FAQ 2018-08-03 04:28:37 +00:00
M66B
268f94e77e Updated description 2018-08-03 04:21:42 +00:00
M66B
1e25a9f97c Added reply-to address entry to identity 2018-08-02 22:26:56 +00:00
M66B
c37f5b934d 0.3 release 2018-08-02 21:13:56 +00:00
M66B
2597b5e825 Added provider mailbox.org 2018-08-02 21:12:29 +00:00
M66B
921b31abec Removed countermeasures, rewritten operation handling 2018-08-02 20:52:06 +00:00
M66B
bb63ef2cfa Prevent spam/archive for messages in outbox 2018-08-02 20:49:11 +00:00
M66B
7feac257d1 Catch exceptions 2018-08-02 20:45:16 +00:00
M66B
8c1ad78caf Fixed reply to messages without sender 2018-08-02 20:32:47 +00:00
M66B
1240b29404 Fixed check for missing to recipient 2018-08-02 20:19:12 +00:00
M66B
7a3a226102 Added FAQs 2018-08-02 18:20:36 +00:00
M66B
930fb92327 Skip operation seen for local only messages
(outbox, unappended drafts)
2018-08-02 17:58:40 +00:00
M66B
3fe434b60c Add debug info to remote drafts 2018-08-02 17:42:57 +00:00
M66B
539ee934ea Fixed grouping of messages without thread 2018-08-02 17:39:24 +00:00
M66B
0b83d1eb67 Layout improvement 2018-08-02 17:39:04 +00:00
210 changed files with 3255 additions and 974 deletions

Binary file not shown.

33
FAQ.md
View File

@@ -5,23 +5,30 @@ Frequently Asked Questions
--------------------------
<a name="FAQ1"></a>
**(1) Which email providers are supported?**
* Gmail
* Outlook
<a name="FAQ2"></a>
**(2) What is a valid security certificate?**
Valid security certificates are officially signed (not self signed) and have matching a host name.
<a name="FAQ3"></a>
**(3) Which permissions are needed and why?**
**(1) Which permissions are needed and why?**
* Full network access (INTERNET): to send and receive email
* View network connections (ACCESS_NETWORK_STATE): to monitor internet connectivity changes
* Run at startup (RECEIVE_BOOT_COMPLETED): to start monitoring on device start
<a name="FAQ2"></a>
**(2) What are operations?**
The low priority status bar notification shows the number of pending operations, which can be:
* Mark message as seen/unseen in remote folder
* Add message to remote folder
* Move message to another remote folder
* Delete message from remote folder
* Send message
* Download attachment
<a name="FAQ3"></a>
**(3) What is a valid security certificate?**
Valid security certificates are officially signed (not self signed) and have matching a host name.
<br>
If you have another question, you can use [this forum](https://forum.xda-developers.com/).
If you have another question, you can use [this forum](https://forum.xda-developers.com/android/apps-games/source-email-t3824168).

View File

@@ -5,7 +5,7 @@ Simple, secure and efficient email app
This email app might be for you if your current email app:
* takes long to show messages
* takes long to show or receive messages
* can manage only one mailbox
* cannot show related messages
* cannot work offline
@@ -47,8 +47,8 @@ Secure
Efficient
---------
* No [POP](https://en.wikipedia.org/wiki/Post_Office_Protocol) support
* [IMAP IDLE](https://en.wikipedia.org/wiki/IMAP_IDLE) required
* [IMAP IDLE](https://en.wikipedia.org/wiki/IMAP_IDLE) required (no [POP](https://en.wikipedia.org/wiki/Post_Office_Protocol) support)
* Built with latest development tools and libraries
* Android 6 Marshmallow or later required
This app starts a foreground service with a low priority status bar notification to make sure you'll never miss new email.

View File

@@ -6,9 +6,15 @@ android {
applicationId "eu.faircode.email"
minSdkVersion 23
targetSdkVersion 28
versionCode 2
versionName "0.2"
versionCode 4
versionName "0.4"
archivesBaseName = "SafeEmail-v$versionName"
javaCompileOptions {
annotationProcessorOptions {
arguments = ["room.schemaLocation": "$projectDir/schemas".toString()]
}
}
}
buildTypes {

View File

@@ -20,9 +20,13 @@
# hide the original source file name.
-renamesourcefileattribute SourceFile
#App
-keep class eu.faircode.email.**
-keepnames class eu.faircode.email.** { *; }
#Support library
-keep class android.support.v7.app.AppCompatViewInflater{ <init>(...); }
#JavaMail
-dontshrink
-keep class javax.** {*;}

View File

@@ -0,0 +1,639 @@
{
"formatVersion": 1,
"database": {
"version": 2,
"identityHash": "28de77abd152c62bbdcca05efc2a6f59",
"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, `host` TEXT NOT NULL, `port` INTEGER NOT NULL, `starttls` INTEGER NOT NULL, `user` TEXT NOT NULL, `password` TEXT NOT NULL, `primary` INTEGER NOT NULL, `synchronize` INTEGER NOT NULL)",
"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": "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": "primary",
"columnName": "primary",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "synchronize",
"columnName": "synchronize",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": true
},
"indices": [],
"foreignKeys": []
},
{
"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, `primary` INTEGER NOT NULL, `synchronize` INTEGER NOT NULL)",
"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": "primary",
"columnName": "primary",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "synchronize",
"columnName": "synchronize",
"affinity": "INTEGER",
"notNull": true
}
],
"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, 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
}
],
"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, `body` TEXT, `sent` INTEGER, `received` INTEGER NOT NULL, `seen` INTEGER NOT NULL, `ui_seen` INTEGER NOT NULL, `ui_hide` INTEGER NOT NULL, 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": "body",
"columnName": "body",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "sent",
"columnName": "sent",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "received",
"columnName": "received",
"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
}
],
"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_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`)"
}
],
"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, `type` TEXT NOT NULL, `name` TEXT, `content` BLOB, 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": "type",
"columnName": "type",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "content",
"columnName": "content",
"affinity": "BLOB",
"notNull": false
}
],
"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, `message` INTEGER NOT NULL, `name` TEXT NOT NULL, `args` TEXT, 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": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "args",
"columnName": "args",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": true
},
"indices": [
{
"name": "index_operation_message",
"unique": false,
"columnNames": [
"message"
],
"createSql": "CREATE INDEX `index_operation_message` ON `${TABLE_NAME}` (`message`)"
}
],
"foreignKeys": [
{
"table": "message",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"message"
],
"referencedColumns": [
"id"
]
}
]
}
],
"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, \"28de77abd152c62bbdcca05efc2a6f59\")"
]
}
}

View File

@@ -17,6 +17,14 @@
android:supportsRtl="true"
android:theme="@style/AppThemeLight">
<!-- do not contact Google servers -->
<meta-data
android:name="android.webkit.WebView.MetricsOptOut"
android:value="true" />
<meta-data
android:name="android.webkit.WebView.EnableSafeBrowsing"
android:value="false" />
<activity android:name=".ActivitySetup" />
<activity android:name=".ActivityView">

View File

@@ -28,7 +28,9 @@ public class ActivityCompose extends ActivityBase implements FragmentManager.OnB
static final int LOADER_COMPOSE_PUT = 2;
static final int LOADER_COMPOSE_DELETE = 3;
static final int REQUEST_CONTACT = 1;
static final int REQUEST_CONTACT_TO = 1;
static final int REQUEST_CONTACT_CC = 2;
static final int REQUEST_CONTACT_BCC = 3;
@Override
protected void onCreate(Bundle savedInstanceState) {
@@ -38,11 +40,13 @@ public class ActivityCompose extends ActivityBase implements FragmentManager.OnB
getSupportFragmentManager().addOnBackStackChangedListener(this);
if (getSupportFragmentManager().getFragments().size() == 0) {
FragmentCompose fragment = new FragmentCompose();
Bundle args = getIntent().getExtras();
if (args == null)
args = new Bundle();
FragmentCompose fragment = new FragmentCompose();
fragment.setArguments(args);
FragmentTransaction fragmentTransaction = getSupportFragmentManager().beginTransaction();
fragmentTransaction.replace(R.id.content_frame, fragment).addToBackStack("compose");
fragmentTransaction.commit();

View File

@@ -64,6 +64,7 @@ public class ActivityView extends ActivityBase implements FragmentManager.OnBack
static final int LOADER_ACCOUNT_PUT = 1;
static final int LOADER_IDENTITY_PUT = 2;
static final int LOADER_FOLDER_PUT = 3;
static final int LOADER_MESSAGES_INIT = 4;
static final int REQUEST_VIEW = 1;
@@ -76,6 +77,7 @@ public class ActivityView extends ActivityBase implements FragmentManager.OnBack
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_view);
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
@@ -211,9 +213,10 @@ public class ActivityView extends ActivityBase implements FragmentManager.OnBack
if (prefs.getBoolean("eula", false)) {
drawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED);
FragmentMessages fragment = new FragmentMessages();
Bundle args = new Bundle();
args.putLong("folder", -1);
FragmentMessages fragment = new FragmentMessages();
fragment.setArguments(args);
FragmentTransaction fragmentTransaction = getSupportFragmentManager().beginTransaction();
@@ -256,9 +259,10 @@ public class ActivityView extends ActivityBase implements FragmentManager.OnBack
}
private void onMenuUnified() {
FragmentMessages fragment = new FragmentMessages();
Bundle args = new Bundle();
args.putLong("folder", -1);
FragmentMessages fragment = new FragmentMessages();
fragment.setArguments(args);
FragmentTransaction fragmentTransaction = getSupportFragmentManager().beginTransaction();
@@ -320,6 +324,8 @@ public class ActivityView extends ActivityBase implements FragmentManager.OnBack
draft.ui_hide = false;
draft.id = db.message().insertMessage(draft);
EntityOperation.queue(ActivityView.this, draft, EntityOperation.ADD);
startActivity(new Intent(ActivityView.this, ActivityCompose.class)
.putExtra("id", draft.id));
}

View File

@@ -0,0 +1,199 @@
package eu.faircode.email;
/*
This file is part of Safe email.
Safe email 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.support.annotation.NonNull;
import android.support.v7.util.DiffUtil;
import android.support.v7.util.ListUpdateCallback;
import android.support.v7.widget.RecyclerView;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class AdapterAttachment extends RecyclerView.Adapter<AdapterAttachment.ViewHolder> {
private Context context;
private ExecutorService executor = Executors.newCachedThreadPool();
private List<EntityAttachment> all = new ArrayList<>();
private List<EntityAttachment> filtered = new ArrayList<>();
public class ViewHolder extends RecyclerView.ViewHolder
implements View.OnClickListener {
View itemView;
TextView tvName;
TextView tvSize;
ImageView ivDownload;
ViewHolder(View itemView) {
super(itemView);
this.itemView = itemView;
tvName = itemView.findViewById(R.id.tvName);
tvSize = itemView.findViewById(R.id.tvSize);
ivDownload = itemView.findViewById(R.id.ivDownload);
}
private void wire() {
itemView.setOnClickListener(this);
ivDownload.setOnClickListener(this);
}
private void unwire() {
itemView.setOnClickListener(null);
ivDownload.setOnClickListener(null);
}
@Override
public void onClick(View view) {
final EntityAttachment attachment = filtered.get(getLayoutPosition());
if (attachment != null && attachment.content == null)
executor.submit(new Runnable() {
@Override
public void run() {
EntityMessage message = DB.getInstance(context).message().getMessage(attachment.message);
EntityOperation.queue(context, message, EntityOperation.ATTACHMENT, attachment.sequence);
}
});
}
}
AdapterAttachment(Context context) {
this.context = context;
setHasStableIds(true);
}
public void set(List<EntityAttachment> attachments) {
Log.i(Helper.TAG, "Set attachments=" + attachments.size());
Collections.sort(attachments, new Comparator<EntityAttachment>() {
@Override
public int compare(EntityAttachment a1, EntityAttachment a2) {
return a1.sequence.compareTo(a2.sequence);
}
});
all.clear();
all.addAll(attachments);
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(AdapterAttachment.this);
}
private class MessageDiffCallback extends DiffUtil.Callback {
private List<EntityAttachment> prev;
private List<EntityAttachment> next;
MessageDiffCallback(List<EntityAttachment> prev, List<EntityAttachment> 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) {
EntityAttachment a1 = prev.get(oldItemPosition);
EntityAttachment a2 = next.get(newItemPosition);
return a1.id.equals(a2.id);
}
@Override
public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) {
EntityAttachment a1 = prev.get(oldItemPosition);
EntityAttachment a2 = next.get(newItemPosition);
return a1.equals(a2);
}
}
@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_attachment, parent, false));
}
@Override
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
holder.unwire();
EntityAttachment attachment = filtered.get(position);
holder.tvName.setText(attachment.name);
holder.tvSize.setVisibility((attachment.content == null ? View.GONE : View.VISIBLE));
holder.ivDownload.setVisibility((attachment.content == null ? View.VISIBLE : View.GONE));
if (attachment.content != null)
holder.tvSize.setText(Helper.humanReadableByteCount(attachment.content.length, false));
holder.wire();
}
}

View File

@@ -119,15 +119,17 @@ public class AdapterFolder extends RecyclerView.Adapter<AdapterFolder.ViewHolder
Collections.sort(folders, new Comparator<TupleFolderEx>() {
@Override
public int compare(TupleFolderEx f1, TupleFolderEx f2) {
if (f1.accountName == null)
if (f2.accountName == null)
return 0;
int s = EntityFolder.isUser(f1.type).compareTo(EntityFolder.isUser(f2.type));
if (s == 0) {
int a = collator.compare(
f1.accountName == null ? "" : f1.accountName,
f2.accountName == null ? "" : f2.accountName);
if (a == 0)
return collator.compare(f1.name, f2.name);
else
return -1;
else if (f2.accountName == null)
return 1;
else
return collator.compare(f1.accountName, f2.accountName);
return a;
} else
return s;
}
});
@@ -224,13 +226,20 @@ public class AdapterFolder extends RecyclerView.Adapter<AdapterFolder.ViewHolder
holder.tvName.setText(context.getString(R.string.title_folder_unseen, name, folder.unseen));
else
holder.tvName.setText(name);
holder.tvName.setTypeface(null, folder.unseen > 0 ? Typeface.BOLD : Typeface.NORMAL);
holder.tvAfter.setText(Integer.toString(folder.after));
holder.tvAfter.setVisibility(folder.synchronize ? View.VISIBLE : View.INVISIBLE);
holder.ivSync.setVisibility(folder.synchronize ? View.VISIBLE : View.INVISIBLE);
holder.tvCount.setText(Integer.toString(folder.messages));
holder.tvType.setText(folder.type);
int resid = context.getResources().getIdentifier(
"title_folder_" + folder.type.toLowerCase(),
"string",
context.getPackageName());
holder.tvType.setText(resid > 0 ? context.getString(resid) : folder.type);
holder.tvAccount.setText(folder.accountName);
holder.wire();

View File

@@ -32,6 +32,7 @@ import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
import java.util.ArrayList;
@@ -56,8 +57,9 @@ public class AdapterMessage extends RecyclerView.Adapter<AdapterMessage.ViewHold
public class ViewHolder extends RecyclerView.ViewHolder
implements View.OnClickListener {
View itemView;
TextView tvAddress;
TextView tvFrom;
TextView tvTime;
ImageView ivAttachments;
TextView tvSubject;
TextView tvCount;
@@ -65,8 +67,9 @@ public class AdapterMessage extends RecyclerView.Adapter<AdapterMessage.ViewHold
super(itemView);
this.itemView = itemView;
tvAddress = itemView.findViewById(R.id.tvAddress);
tvFrom = itemView.findViewById(R.id.tvFrom);
tvTime = itemView.findViewById(R.id.tvTime);
ivAttachments = itemView.findViewById(R.id.ivAttachments);
tvSubject = itemView.findViewById(R.id.tvSubject);
tvCount = itemView.findViewById(R.id.tvCount);
}
@@ -86,22 +89,26 @@ public class AdapterMessage extends RecyclerView.Adapter<AdapterMessage.ViewHold
executor.submit(new Runnable() {
@Override
public void run() {
if (EntityFolder.TYPE_DRAFTS.equals(message.folderType))
context.startActivity(
new Intent(context, ActivityCompose.class)
.putExtra("id", message.id));
else {
if (!message.seen && !message.ui_seen) {
message.ui_seen = !message.ui_seen;
DB.getInstance(context).message().updateMessage(message);
EntityOperation.queue(context, message, EntityOperation.SEEN, message.ui_seen);
}
try {
if (EntityFolder.TYPE_DRAFTS.equals(message.folderType))
context.startActivity(
new Intent(context, ActivityCompose.class)
.putExtra("id", message.id));
else {
if (!message.seen && !message.ui_seen) {
message.ui_seen = !message.ui_seen;
DB.getInstance(context).message().updateMessage(message);
EntityOperation.queue(context, message, EntityOperation.SEEN, message.ui_seen);
}
LocalBroadcastManager lbm = LocalBroadcastManager.getInstance(context);
lbm.sendBroadcast(
new Intent(ActivityView.ACTION_VIEW_MESSAGE)
.putExtra("folder", message.folder)
.putExtra("id", message.id));
LocalBroadcastManager lbm = LocalBroadcastManager.getInstance(context);
lbm.sendBroadcast(
new Intent(ActivityView.ACTION_VIEW_MESSAGE)
.putExtra("folder", message.folder)
.putExtra("id", message.id));
}
} catch (Throwable ex) {
Log.e(Helper.TAG, ex + "\n" + Log.getStackTraceString(ex));
}
}
});
@@ -216,10 +223,10 @@ public class AdapterMessage extends RecyclerView.Adapter<AdapterMessage.ViewHold
TupleMessageEx message = filtered.get(position);
if (EntityFolder.isOutgoing(message.folderType)) {
holder.tvAddress.setText(message.to == null ? null : MessageHelper.getFormattedAddresses(message.to));
holder.tvFrom.setText(MessageHelper.getFormattedAddresses(message.to));
holder.tvTime.setText(DateUtils.getRelativeTimeSpanString(context, message.received));
} else {
holder.tvAddress.setText(message.from == null ? null : MessageHelper.getFormattedAddresses(message.from));
holder.tvFrom.setText(MessageHelper.getFormattedAddresses(message.from));
holder.tvTime.setText(message.sent == null ? null : DateUtils.getRelativeTimeSpanString(context, message.sent));
}
@@ -232,8 +239,9 @@ public class AdapterMessage extends RecyclerView.Adapter<AdapterMessage.ViewHold
boolean unseen = (message.thread == null ? !message.seen : message.unseen > 0);
int visibility = (unseen ? Typeface.BOLD : Typeface.NORMAL);
holder.tvAddress.setTypeface(null, visibility);
holder.tvFrom.setTypeface(null, visibility);
holder.tvTime.setTypeface(null, visibility);
holder.ivAttachments.setVisibility(message.attachments > 0 ? View.VISIBLE : View.GONE);
holder.tvSubject.setTypeface(null, visibility);
holder.tvCount.setTypeface(null, visibility);

View File

@@ -20,6 +20,57 @@ package eu.faircode.email;
*/
import android.app.Application;
import android.util.Log;
import java.util.Date;
import javax.mail.Address;
import javax.mail.internet.InternetAddress;
public class ApplicationEx extends Application {
private Thread.UncaughtExceptionHandler prev = null;
@Override
public void onCreate() {
super.onCreate();
prev = Thread.getDefaultUncaughtExceptionHandler();
Thread.setDefaultUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {
@Override
public void uncaughtException(Thread thread, Throwable ex) {
Log.w(Helper.TAG, "Handling crash");
DB db = null;
try {
db = DB.getBlockingInstance(ApplicationEx.this);
EntityFolder drafts = db.folder().getPrimaryDraftFolder();
if (drafts != null) {
Address to = new InternetAddress("marcel+email@faircode.eu", "FairCode");
EntityMessage draft = new EntityMessage();
draft.account = drafts.account;
draft.folder = drafts.id;
draft.to = MessageHelper.encodeAddresses(new Address[]{to});
draft.subject = BuildConfig.APPLICATION_ID + " crash info";
draft.body = "<pre>" + ex.toString().replaceAll("\\r?\\n", "<br />") + "</pre>";
draft.received = new Date().getTime();
draft.seen = false;
draft.ui_seen = false;
draft.ui_hide = false;
draft.id = db.message().insertMessage(draft);
Log.w(Helper.TAG, "Crash info stored as draft");
}
} catch (Throwable e1) {
Log.e(Helper.TAG, e1 + "\n" + Log.getStackTraceString(e1));
} finally {
if (db != null)
db.close();
}
if (prev != null)
prev.uncaughtException(thread, ex);
}
});
}
}

View File

@@ -37,9 +37,10 @@ import android.util.Log;
EntityAccount.class,
EntityFolder.class,
EntityMessage.class,
EntityAttachment.class,
EntityOperation.class
},
version = 1,
version = 2,
exportSchema = true
)
@@ -53,6 +54,8 @@ public abstract class DB extends RoomDatabase {
public abstract DaoMessage message();
public abstract DaoAttachment attachment();
public abstract DaoOperation operation();
private static DB sInstance;
@@ -65,9 +68,13 @@ public abstract class DB extends RoomDatabase {
return sInstance;
}
public static DB getBlockingInstance(Context context) {
return migrate(Room.databaseBuilder(context.getApplicationContext(), DB.class, DB_NAME).allowMainThreadQueries());
}
private static DB migrate(RoomDatabase.Builder<DB> builder) {
return builder
//.addMigrations(MIGRATION_1_2)
.addMigrations(MIGRATION_1_2)
.build();
}
@@ -75,7 +82,14 @@ public abstract class DB extends RoomDatabase {
@Override
public void migrate(SupportSQLiteDatabase db) {
Log.i(Helper.TAG, "DB migration from version " + startVersion + " to " + endVersion);
db.execSQL("ALTER TABLE message ADD COLUMN error TEXT");
db.execSQL("CREATE TABLE IF NOT EXISTS `attachment`" +
" (`id` INTEGER PRIMARY KEY AUTOINCREMENT" +
", `message` INTEGER NOT NULL" +
", `sequence` INTEGER NOT NULL" +
", `type` TEXT NOT NULL, `name` TEXT" +
", `content` BLOB, FOREIGN KEY(`message`) REFERENCES `message`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )");
db.execSQL("CREATE INDEX `index_attachment_message` ON `attachment` (`message`)");
db.execSQL("CREATE UNIQUE INDEX `index_attachment_message_sequence` ON `attachment` (`message`, `sequence`)");
}
};

View File

@@ -0,0 +1,44 @@
package eu.faircode.email;
/*
This file is part of Safe email.
Safe email 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.arch.lifecycle.LiveData;
import android.arch.persistence.room.Dao;
import android.arch.persistence.room.Insert;
import android.arch.persistence.room.OnConflictStrategy;
import android.arch.persistence.room.Query;
import android.arch.persistence.room.Update;
import java.util.List;
@Dao
public interface DaoAttachment {
@Query("SELECT * FROM attachment WHERE message = :message")
LiveData<List<EntityAttachment>> liveAttachments(long message);
@Query("SELECT * FROM attachment WHERE message = :message AND sequence = :sequence")
EntityAttachment getAttachment(long message, int sequence);
@Insert(onConflict = OnConflictStrategy.REPLACE)
long insertAttachment(EntityAttachment attachment);
@Update
void updateAttachment(EntityAttachment attachment);
}

View File

@@ -33,26 +33,32 @@ public interface DaoMessage {
@Query("SELECT message.*, folder.name as folderName, folder.type as folderType" +
", (SELECT COUNT(m.id) FROM message m WHERE m.account = message.account AND m.thread = message.thread) AS count" +
", (SELECT COUNT(m.id) FROM message m WHERE m.account = message.account AND m.thread = message.thread AND NOT m.ui_seen) AS unseen" +
", (SELECT COUNT(a.id) FROM attachment a WHERE a.message = message.id) AS attachments" +
" FROM folder" +
" JOIN message ON folder = folder.id" +
" WHERE folder.type = '" + EntityFolder.TYPE_INBOX + "'" +
" AND NOT ui_hide" +
" AND received IN (SELECT MAX(m.received) FROM message m WHERE m.folder = message.folder GROUP BY m.thread)")
" AND received IN (SELECT MAX(m.received) FROM message m WHERE m.folder = message.folder" +
" GROUP BY CASE WHEN m.thread IS NULL THEN m.id ELSE m.thread END)")
// in theory the message id and thread could be the same
LiveData<List<TupleMessageEx>> liveUnifiedInbox();
@Query("SELECT message.*, folder.name as folderName, folder.type as folderType" +
", (SELECT COUNT(m.id) FROM message m WHERE m.account = message.account AND m.thread = message.thread) AS count" +
", (SELECT COUNT(m.id) FROM message m WHERE m.account = message.account AND m.thread = message.thread AND NOT m.ui_seen) AS unseen" +
", (SELECT COUNT(a.id) FROM attachment a WHERE a.message = message.id) AS attachments" +
" FROM folder" +
" JOIN message ON folder = folder.id" +
" WHERE folder.id = :folder" +
" AND NOT ui_hide" +
" AND received IN (SELECT MAX(m.received) FROM message m WHERE m.folder = message.folder GROUP BY m.thread)")
" AND received IN (SELECT MAX(m.received) FROM message m WHERE m.folder = message.folder" +
" GROUP BY CASE WHEN m.thread IS NULL THEN m.id ELSE m.thread END)")
LiveData<List<TupleMessageEx>> liveMessages(long folder);
@Query("SELECT message.*, folder.name as folderName, folder.type as folderType" +
", (SELECT COUNT(m.id) FROM message m WHERE m.account = message.account AND m.thread = message.thread) AS count" +
", (SELECT COUNT(m.id) FROM message m WHERE m.account = message.account AND m.thread = message.thread AND NOT m.ui_seen) AS unseen" +
", (SELECT COUNT(a.id) FROM attachment a WHERE a.message = message.id) AS attachments" +
" FROM message" +
" JOIN folder ON folder.id = message.folder" +
" JOIN message m1 ON m1.id = :msgid AND m1.account = message.account AND m1.thread = message.thread" +
@@ -68,6 +74,7 @@ public interface DaoMessage {
@Query("SELECT message.*, folder.name as folderName, folder.type as folderType" +
", (SELECT COUNT(m.id) FROM message m WHERE m.account = message.account AND m.thread = message.thread) AS count" +
", (SELECT COUNT(m.id) FROM message m WHERE m.account = message.account AND m.thread = message.thread AND NOT m.ui_seen) AS unseen" +
", (SELECT COUNT(a.id) FROM attachment a WHERE a.message = message.id) AS attachments" +
" FROM message" +
" JOIN folder ON folder.id = message.folder" +
" WHERE message.id = :id")

View File

@@ -21,10 +21,13 @@ package eu.faircode.email;
import android.arch.persistence.room.Entity;
import android.arch.persistence.room.ForeignKey;
import android.arch.persistence.room.Ignore;
import android.arch.persistence.room.Index;
import android.arch.persistence.room.PrimaryKey;
import android.support.annotation.NonNull;
import javax.mail.BodyPart;
import static android.arch.persistence.room.ForeignKey.CASCADE;
@Entity(
@@ -33,7 +36,8 @@ import static android.arch.persistence.room.ForeignKey.CASCADE;
@ForeignKey(childColumns = "message", entity = EntityMessage.class, parentColumns = "id", onDelete = CASCADE)
},
indices = {
@Index(value = {"message"})
@Index(value = {"message"}),
@Index(value = {"message", "sequence"}, unique = true)
}
)
public class EntityAttachment {
@@ -44,7 +48,12 @@ public class EntityAttachment {
@NonNull
public Long message;
@NonNull
public String type;
public Integer sequence;
public String name;
@NonNull
public String type;
public byte[] content;
@Ignore
BodyPart part;
}

View File

@@ -54,25 +54,29 @@ public class EntityFolder {
static final String TYPE_SENT = "Sent";
static final String TYPE_USER = "User";
static final List<String> STANDARD_FOLDER_ATTR = Arrays.asList(
static final List<String> SYSTEM_FOLDER_ATTR = Arrays.asList(
"All",
"Drafts",
"Trash",
"Junk",
"Sent"
);
static final List<String> STANDARD_FOLDER_TYPE = Arrays.asList(
static final List<String> SYSTEM_FOLDER_TYPE = Arrays.asList(
TYPE_ARCHIVE,
TYPE_DRAFTS,
TYPE_TRASH,
TYPE_JUNK,
TYPE_SENT
); // Must match STANDARD_FOLDER_ATTR
); // Must match SYSTEM_FOLDER_ATTR
static boolean isOutgoing(String type) {
return (TYPE_OUTBOX.equals(type) || TYPE_DRAFTS.equals(type) || TYPE_SENT.equals(type));
}
static Boolean isUser(String type) {
return TYPE_USER.equals(type);
}
@PrimaryKey(autoGenerate = true)
public Long id;
public Long account; // Outbox = null

View File

@@ -58,6 +58,7 @@ public class EntityOperation {
public static final String MOVE = "move";
public static final String DELETE = "delete";
public static final String SEND = "send";
public static final String ATTACHMENT = "attachment";
static void queue(Context context, EntityMessage message, String name) {
JSONArray jsonArray = new JSONArray();
@@ -74,8 +75,16 @@ public class EntityOperation {
DaoOperation dao = DB.getInstance(context).operation();
int purged = 0;
if (SEEN.equals(name))
if (SEEN.equals(name)) {
if (message.uid == null) {
// local message
return;
}
purged = dao.deleteOperations(message.id, name);
} else if (DELETE.equals(name)) {
if (message.uid == null)
return;
}
EntityOperation operation = new EntityOperation();
operation.message = message.id;

View File

@@ -24,6 +24,7 @@ import android.content.Context;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.design.widget.TextInputLayout;
import android.support.v4.app.Fragment;
import android.support.v4.app.LoaderManager;
import android.support.v4.content.AsyncTaskLoader;
@@ -63,7 +64,7 @@ public class FragmentAccount extends Fragment {
private EditText etHost;
private EditText etPort;
private EditText etUser;
private EditText etPassword;
private TextInputLayout tilPassword;
private CheckBox cbPrimary;
private CheckBox cbSynchronize;
private Button btnOk;
@@ -96,7 +97,7 @@ public class FragmentAccount extends Fragment {
etHost = view.findViewById(R.id.etHost);
etPort = view.findViewById(R.id.etPort);
etUser = view.findViewById(R.id.etUser);
etPassword = view.findViewById(R.id.etPassword);
tilPassword = view.findViewById(R.id.tilPassword);
cbPrimary = view.findViewById(R.id.cbPrimary);
cbSynchronize = view.findViewById(R.id.cbSynchronize);
btnOk = view.findViewById(R.id.btnOk);
@@ -138,7 +139,7 @@ public class FragmentAccount extends Fragment {
args.putString("host", etHost.getText().toString());
args.putString("port", etPort.getText().toString());
args.putString("user", etUser.getText().toString());
args.putString("password", etPassword.getText().toString());
args.putString("password", tilPassword.getEditText().getText().toString());
args.putBoolean("primary", cbPrimary.isChecked());
args.putBoolean("synchronize", cbSynchronize.isChecked());
@@ -153,7 +154,7 @@ public class FragmentAccount extends Fragment {
etHost.setText(account == null ? null : account.host);
etPort.setText(account == null ? null : Long.toString(account.port));
etUser.setText(account == null ? null : account.user);
etPassword.setText(account == null ? null : account.password);
tilPassword.getEditText().setText(account == null ? null : account.password);
cbPrimary.setChecked(account == null ? true : account.primary);
cbSynchronize.setChecked(account == null ? true : account.synchronize);
}
@@ -217,33 +218,58 @@ public class FragmentAccount extends Fragment {
if (!istore.hasCapability("IDLE"))
throw new MessagingException(getContext().getString(R.string.title_no_idle));
// Find system folders
boolean drafts = false;
for (Folder ifolder : istore.getDefaultFolder().list("*")) {
String type = null;
// First check folder attributes
String[] attrs = ((IMAPFolder) ifolder).getAttributes();
for (String attr : attrs) {
if (attr.startsWith("\\")) {
int index = EntityFolder.STANDARD_FOLDER_ATTR.indexOf(attr.substring(1));
int index = EntityFolder.SYSTEM_FOLDER_ATTR.indexOf(attr.substring(1));
if (index >= 0) {
EntityFolder folder = new EntityFolder();
folder.name = ifolder.getFullName();
folder.type = EntityFolder.STANDARD_FOLDER_TYPE.get(index);
folder.synchronize = standard_sync.contains(folder.type);
folder.after = DEFAULT_STANDARD_SYNC;
folders.add(folder);
Log.i(Helper.TAG, "Standard folder=" + folder.name +
" type=" + folder.type + " attr=" + TextUtils.join(",", attrs));
if (EntityFolder.TYPE_DRAFTS.equals(folder.type))
drafts = true;
type = EntityFolder.SYSTEM_FOLDER_TYPE.get(index);
break;
}
}
}
// Next check folder full name
if (type == null) {
String fullname = ifolder.getFullName();
for (String attr : EntityFolder.SYSTEM_FOLDER_ATTR)
if (attr.equals(fullname)) {
int index = EntityFolder.SYSTEM_FOLDER_ATTR.indexOf(attr);
type = EntityFolder.SYSTEM_FOLDER_TYPE.get(index);
break;
}
}
if (type != null) {
EntityFolder folder = new EntityFolder();
folder.name = ifolder.getFullName();
folder.type = type;
folder.synchronize = standard_sync.contains(folder.type);
folder.after = DEFAULT_STANDARD_SYNC;
folders.add(folder);
Log.i(Helper.TAG, account.name +
" system=" + folder.name +
" type=" + folder.type + " attr=" + TextUtils.join(",", attrs));
if (EntityFolder.TYPE_DRAFTS.equals(folder.type))
drafts = true;
}
}
if (!drafts) {
EntityFolder folder = new EntityFolder();
folder.name = getContext().getString(R.string.title_local_drafts);
folder.type = EntityFolder.TYPE_DRAFTS;
folder.synchronize = false;
folder.after = 0;
folders.add(folder);
}
if (!drafts)
throw new MessagingException(getContext().getString(R.string.title_no_drafts));
} finally {
if (istore != null)
istore.close();
@@ -253,30 +279,31 @@ public class FragmentAccount extends Fragment {
if (account.primary)
db.account().resetPrimary();
if (update)
db.account().updateAccount(account);
else
try {
db.beginTransaction();
try {
db.beginTransaction();
if (update)
db.account().updateAccount(account);
else
account.id = db.account().insertAccount(account);
EntityFolder inbox = new EntityFolder();
inbox.name = "INBOX";
inbox.type = EntityFolder.TYPE_INBOX;
inbox.synchronize = true;
inbox.after = DEFAULT_INBOX_SYNC;
folders.add(0, inbox);
EntityFolder inbox = new EntityFolder();
inbox.name = "INBOX";
inbox.type = EntityFolder.TYPE_INBOX;
inbox.synchronize = true;
inbox.after = DEFAULT_INBOX_SYNC;
folders.add(0, inbox);
for (EntityFolder folder : folders) {
for (EntityFolder folder : folders)
if (db.folder().getFolder(account.id, folder.name) == null) {
folder.account = account.id;
Log.i(Helper.TAG, "Creating folder=" + folder.name + " (" + folder.type + ")");
folder.id = db.folder().insertFolder(folder);
}
db.setTransactionSuccessful();
} finally {
db.endTransaction();
}
db.setTransactionSuccessful();
} finally {
db.endTransaction();
}
ServiceSynchronize.restart(getContext(), "account");
@@ -292,7 +319,7 @@ public class FragmentAccount extends Fragment {
@NonNull
@Override
public Loader<Throwable> onCreateLoader(int id, Bundle args) {
PutLoader loader = new PutLoader(getActivity());
PutLoader loader = new PutLoader(getContext());
loader.setArgs(args);
return loader;
}

View File

@@ -40,6 +40,8 @@ import android.text.TextUtils;
import android.text.method.LinkMovementMethod;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
@@ -69,13 +71,18 @@ public class FragmentCompose extends Fragment {
private long rid = -1;
private Spinner spFrom;
private ImageView ivIdentyAdd;
private ImageView ivIdentityAdd;
private EditText etTo;
private ImageView ivContactAdd;
private ImageView ivToAdd;
private EditText etCc;
private ImageView ivCcAdd;
private EditText etBcc;
private ImageView ivBccAdd;
private EditText etSubject;
private EditText etBody;
private BottomNavigationView bottom_navigation;
private ProgressBar pbWait;
private Group grpCc;
private Group grpReady;
@Override
@@ -90,45 +97,67 @@ public class FragmentCompose extends Fragment {
// Get controls
spFrom = view.findViewById(R.id.spFrom);
ivIdentyAdd = view.findViewById(R.id.ivIdentyAdd);
ivIdentityAdd = view.findViewById(R.id.ivIdentityAdd);
etTo = view.findViewById(R.id.etTo);
ivContactAdd = view.findViewById(R.id.ivContactAdd);
ivToAdd = view.findViewById(R.id.ivToAdd);
etCc = view.findViewById(R.id.etCc);
ivCcAdd = view.findViewById(R.id.ivCcAdd);
etBcc = view.findViewById(R.id.etBcc);
ivBccAdd = view.findViewById(R.id.ivBccAdd);
etSubject = view.findViewById(R.id.etSubject);
etBody = view.findViewById(R.id.etBody);
bottom_navigation = view.findViewById(R.id.bottom_navigation);
pbWait = view.findViewById(R.id.pbWait);
grpCc = view.findViewById(R.id.grpCc);
grpReady = view.findViewById(R.id.grpReady);
grpCc.setVisibility(View.GONE);
etBody.setMovementMethod(LinkMovementMethod.getInstance());
// Wire controls
ivIdentyAdd.setOnClickListener(new View.OnClickListener() {
ivIdentityAdd.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Bundle args = new Bundle();
args.putLong("id", -1);
FragmentIdentity fragment = new FragmentIdentity();
fragment.setArguments(args);
FragmentTransaction fragmentTransaction = getFragmentManager().beginTransaction();
fragmentTransaction.replace(R.id.content_frame, fragment).addToBackStack("identity");
fragmentTransaction.commit();
}
});
ivContactAdd.setOnClickListener(new View.OnClickListener() {
ivToAdd.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Intent intent = new Intent(Intent.ACTION_PICK, ContactsContract.CommonDataKinds.Email.CONTENT_URI);
startActivityForResult(intent, ActivityCompose.REQUEST_CONTACT);
startActivityForResult(intent, ActivityCompose.REQUEST_CONTACT_TO);
}
});
ivCcAdd.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Intent intent = new Intent(Intent.ACTION_PICK, ContactsContract.CommonDataKinds.Email.CONTENT_URI);
startActivityForResult(intent, ActivityCompose.REQUEST_CONTACT_CC);
}
});
ivBccAdd.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Intent intent = new Intent(Intent.ACTION_PICK, ContactsContract.CommonDataKinds.Email.CONTENT_URI);
startActivityForResult(intent, ActivityCompose.REQUEST_CONTACT_BCC);
}
});
bottom_navigation.setOnNavigationItemSelectedListener(new BottomNavigationView.OnNavigationItemSelectedListener() {
@Override
public boolean onNavigationItemSelected(@NonNull MenuItem item) {
bottom_navigation.setEnabled(false);
switch (item.getItemId()) {
case R.id.action_delete:
actionDelete(id);
@@ -145,11 +174,13 @@ public class FragmentCompose extends Fragment {
}
});
setHasOptionsMenu(true);
// Initialize
grpReady.setVisibility(View.GONE);
pbWait.setVisibility(View.VISIBLE);
bottom_navigation.getMenu().findItem(R.id.action_delete).setEnabled(id > 0);
bottom_navigation.setEnabled(false);
bottom_navigation.getMenu().findItem(R.id.action_delete).setVisible(id > 0);
bottom_navigation.getMenu().setGroupEnabled(0, false);
DB.getInstance(getContext()).identity().liveIdentities(true).observe(getActivity(), new Observer<List<EntityIdentity>>() {
@Override
@@ -185,9 +216,30 @@ public class FragmentCompose extends Fragment {
((AppCompatActivity) getActivity()).getSupportActionBar().setSubtitle(R.string.title_compose);
}
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
inflater.inflate(R.menu.menu_cc, menu);
super.onCreateOptionsMenu(menu, inflater);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.menu_cc:
onMenuCc();
return true;
default:
return super.onOptionsItemSelected(item);
}
}
private void onMenuCc() {
grpCc.setVisibility(grpCc.getVisibility() == View.GONE ? View.VISIBLE : View.GONE);
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == ActivityCompose.REQUEST_CONTACT && resultCode == RESULT_OK) {
if (resultCode == RESULT_OK) {
Cursor cursor = null;
try {
cursor = getContext().getContentResolver().query(data.getData(),
@@ -202,12 +254,26 @@ public class FragmentCompose extends Fragment {
String email = cursor.getString(colEmail);
String name = cursor.getString(colName);
String text = null;
if (requestCode == ActivityCompose.REQUEST_CONTACT_TO)
text = etTo.getText().toString();
else if (requestCode == ActivityCompose.REQUEST_CONTACT_CC)
text = etCc.getText().toString();
else if (requestCode == ActivityCompose.REQUEST_CONTACT_BCC)
text = etBcc.getText().toString();
InternetAddress address = new InternetAddress(email, name);
StringBuilder sb = new StringBuilder(etTo.getText().toString());
StringBuilder sb = new StringBuilder(text);
if (sb.length() > 0)
sb.append("; ");
sb.append(address.toString());
etTo.setText(sb.toString());
if (requestCode == ActivityCompose.REQUEST_CONTACT_TO)
etTo.setText(sb.toString());
else if (requestCode == ActivityCompose.REQUEST_CONTACT_CC)
etCc.setText(sb.toString());
else if (requestCode == ActivityCompose.REQUEST_CONTACT_BCC)
etBcc.setText(sb.toString());
}
} catch (Throwable ex) {
Log.e(Helper.TAG, ex + "\n" + Log.getStackTraceString(ex));
@@ -220,13 +286,15 @@ public class FragmentCompose extends Fragment {
}
private void actionDelete(final long id) {
bottom_navigation.getMenu().setGroupEnabled(0, false);
Bundle args = new Bundle();
args.putLong("id", id);
getLoaderManager().restartLoader(ActivityCompose.LOADER_COMPOSE_DELETE, args, deleteLoaderCallbacks).forceLoad();
}
private void actionPut(long id, boolean send) {
bottom_navigation.setEnabled(false);
bottom_navigation.getMenu().setGroupEnabled(0, false);
EntityIdentity identity = (EntityIdentity) spFrom.getSelectedItem();
@@ -236,6 +304,8 @@ public class FragmentCompose extends Fragment {
args.putString("thread", FragmentCompose.this.thread);
args.putLong("rid", FragmentCompose.this.rid);
args.putString("to", etTo.getText().toString());
args.putString("cc", etCc.getText().toString());
args.putString("bcc", etBcc.getText().toString());
args.putString("subject", etSubject.getText().toString());
args.putString("body", etBody.getText().toString());
args.putBoolean("send", send);
@@ -257,63 +327,68 @@ public class FragmentCompose extends Fragment {
@Nullable
@Override
public Bundle loadInBackground() {
String action = args.getString("action");
long id = args.getLong("id", -1);
EntityMessage msg = DB.getInstance(getContext()).message().getMessage(id);
Bundle result = new Bundle();
result.putString("action", action);
try {
String action = args.getString("action");
long id = args.getLong("id", -1);
EntityMessage msg = DB.getInstance(getContext()).message().getMessage(id);
if (msg != null) {
if (msg.identity != null)
result.putLong("iid", msg.identity);
if (msg.replying != null)
result.putLong("rid", msg.replying);
result.putString("thread", msg.thread);
result.putString("subject", msg.subject);
result.putString("body", msg.body);
}
result.putString("action", action);
if (TextUtils.isEmpty(action)) {
if (msg != null) {
result.putString("from", msg.from);
result.putString("to", msg.to);
if (msg.identity != null)
result.putLong("iid", msg.identity);
if (msg.replying != null)
result.putLong("rid", msg.replying);
result.putString("cc", msg.cc);
result.putString("bcc", msg.bcc);
result.putString("thread", msg.thread);
result.putString("subject", msg.subject);
result.putString("body", msg.body);
}
} else if ("reply".equals(action)) {
String to = null;
if (msg != null)
try {
Address[] reply = MessageHelper.decodeAddresses(msg.reply);
to = (reply.length == 0 ? msg.from : msg.reply);
} catch (Throwable ex) {
Log.e(Helper.TAG, ex + "\n" + Log.getStackTraceString(ex));
}
result.putLong("rid", msg.id);
result.putString("from", msg.to);
result.putString("to", to);
} else if ("reply_all".equals(action)) {
String to = null;
if (msg != null) {
try {
Address[] from = MessageHelper.decodeAddresses(msg.from);
Address[] reply = MessageHelper.decodeAddresses(msg.reply);
Address[] cc = MessageHelper.decodeAddresses(msg.cc);
List<Address> addresses = new ArrayList<>();
addresses.addAll(Arrays.asList(reply.length == 0 ? from : reply));
addresses.addAll(Arrays.asList(cc));
to = MessageHelper.encodeAddresses(addresses.toArray(new Address[0]));
} catch (Throwable ex) {
Log.e(Helper.TAG, ex + "\n" + Log.getStackTraceString(ex));
}
}
result.putLong("rid", msg.id);
result.putString("from", msg.to);
result.putString("to", to);
} else if ("forward".equals(action)) {
result.putString("from", msg.to);
result.putString("to", null);
}
if (TextUtils.isEmpty(action)) {
if (msg != null) {
result.putString("from", msg.from);
result.putString("to", msg.to);
}
} else if ("reply".equals(action)) {
String to = null;
if (msg != null)
try {
Address[] reply = MessageHelper.decodeAddresses(msg.reply);
to = (reply.length == 0 ? msg.from : msg.reply);
} catch (Throwable ex) {
Log.e(Helper.TAG, ex + "\n" + Log.getStackTraceString(ex));
}
result.putLong("rid", msg.id);
result.putString("from", msg.to);
result.putString("to", to);
} else if ("reply_all".equals(action)) {
String to = null;
if (msg != null) {
try {
Address[] from = MessageHelper.decodeAddresses(msg.from);
Address[] reply = MessageHelper.decodeAddresses(msg.reply);
Address[] cc = MessageHelper.decodeAddresses(msg.cc);
List<Address> addresses = new ArrayList<>();
addresses.addAll(Arrays.asList(reply.length == 0 ? from : reply));
addresses.addAll(Arrays.asList(cc));
to = MessageHelper.encodeAddresses(addresses.toArray(new Address[0]));
} catch (Throwable ex) {
Log.e(Helper.TAG, ex + "\n" + Log.getStackTraceString(ex));
}
}
result.putLong("rid", msg.id);
result.putString("from", msg.to);
result.putString("to", to);
} else if ("forward".equals(action)) {
result.putString("from", msg.to);
result.putString("to", null);
}
} catch (Throwable ex) {
Log.e(Helper.TAG, ex + "\n" + Log.getStackTraceString(ex));
}
return result;
}
}
@@ -322,13 +397,13 @@ public class FragmentCompose extends Fragment {
@NonNull
@Override
public Loader<Bundle> onCreateLoader(int id, @Nullable Bundle args) {
GetLoader loader = new GetLoader(getActivity());
GetLoader loader = new GetLoader(getContext());
loader.setArgs(args);
return loader;
}
@Override
public void onLoadFinished(@NonNull Loader<Bundle> loader, final Bundle result) {
public void onLoadFinished(@NonNull Loader<Bundle> loader, Bundle result) {
getLoaderManager().destroyLoader(loader.getId());
long iid = result.getLong("iid", -1);
@@ -336,6 +411,8 @@ public class FragmentCompose extends Fragment {
String thread = result.getString("thread");
String from = result.getString("from");
String to = result.getString("to");
String cc = result.getString("cc");
String bcc = result.getString("bcc");
String subject = result.getString("subject");
String body = result.getString("body");
String action = result.getString("action");
@@ -360,9 +437,11 @@ public class FragmentCompose extends Fragment {
// Prevent changed fields from being overwritten
once = true;
etCc.setText(TextUtils.join(", ", MessageHelper.decodeAddresses(cc)));
etBcc.setText(TextUtils.join(", ", MessageHelper.decodeAddresses(bcc)));
if (action == null) {
if (to != null)
etTo.setText(TextUtils.join(", ", MessageHelper.decodeAddresses(to)));
etTo.setText(TextUtils.join(", ", MessageHelper.decodeAddresses(to)));
etSubject.setText(subject);
if (body != null)
etBody.setText(Html.fromHtml(HtmlHelper.sanitize(getContext(), body, false)));
@@ -384,7 +463,7 @@ public class FragmentCompose extends Fragment {
}
}
bottom_navigation.setEnabled(true);
bottom_navigation.getMenu().setGroupEnabled(0, true);
}
@Override
@@ -405,15 +484,19 @@ public class FragmentCompose extends Fragment {
@Override
public Throwable loadInBackground() {
long id = args.getLong("id");
DaoMessage message = DB.getInstance(getContext()).message();
EntityMessage draft = message.getMessage(id);
if (draft != null) {
draft.ui_hide = true;
message.updateMessage(draft);
EntityOperation.queue(getContext(), draft, EntityOperation.DELETE);
try {
long id = args.getLong("id");
DaoMessage message = DB.getInstance(getContext()).message();
EntityMessage draft = message.getMessage(id);
if (draft != null) {
draft.ui_hide = true;
message.updateMessage(draft);
EntityOperation.queue(getContext(), draft, EntityOperation.DELETE);
}
return null;
} catch (Throwable ex) {
return ex;
}
return null;
}
}
@@ -421,7 +504,7 @@ public class FragmentCompose extends Fragment {
@NonNull
@Override
public Loader<Throwable> onCreateLoader(int id, @Nullable Bundle args) {
DeleteLoader loader = new DeleteLoader(getActivity());
DeleteLoader loader = new DeleteLoader(getContext());
loader.setArgs(args);
return loader;
}
@@ -433,6 +516,10 @@ public class FragmentCompose extends Fragment {
if (ex == null) {
getFragmentManager().popBackStack();
Toast.makeText(getContext(), R.string.title_draft_deleted, Toast.LENGTH_LONG).show();
} else {
Log.w(Helper.TAG, ex + "\n" + Log.getStackTraceString(ex));
bottom_navigation.getMenu().setGroupEnabled(0, true);
Toast.makeText(getContext(), Helper.formatThrowable(ex), Toast.LENGTH_LONG).show();
}
}
@@ -473,11 +560,15 @@ public class FragmentCompose extends Fragment {
long rid = args.getLong("rid", -1);
String thread = args.getString("thread");
String to = args.getString("to");
String cc = args.getString("cc");
String bcc = args.getString("bcc");
String body = args.getString("body");
String subject = args.getString("subject");
Address afrom = (ident == null ? null : new InternetAddress(ident.email, ident.name));
Address ato[] = (TextUtils.isEmpty(to) ? new Address[0] : InternetAddress.parse(to));
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));
// Build draft
boolean update = (draft != null);
@@ -488,8 +579,10 @@ public class FragmentCompose extends Fragment {
draft.identity = (ident == null ? null : ident.id);
draft.replying = (rid < 0 ? null : rid);
draft.thread = thread;
draft.from = (afrom == null ? null : MessageHelper.encodeAddresses(new Address[]{afrom}));
draft.to = (ato == null ? null : MessageHelper.encodeAddresses(ato));
draft.from = MessageHelper.encodeAddresses(new Address[]{afrom});
draft.to = MessageHelper.encodeAddresses(ato);
draft.cc = MessageHelper.encodeAddresses(acc);
draft.bcc = MessageHelper.encodeAddresses(abcc);
draft.subject = subject;
draft.body = "<pre>" + body.replaceAll("\\r?\\n", "<br />") + "</pre>";
draft.received = new Date().getTime();
@@ -507,28 +600,19 @@ public class FragmentCompose extends Fragment {
if (send) {
if (draft.identity == null)
throw new MessagingException(getContext().getString(R.string.title_from_missing));
if (draft.to == null)
if (draft.to == null && draft.cc == null && draft.bcc == null)
throw new MessagingException(getContext().getString(R.string.title_to_missing));
// Get outbox
EntityFolder outbox = folder.getOutbox();
if (outbox == null) {
outbox = new EntityFolder();
outbox.name = "OUTBOX";
outbox.type = EntityFolder.TYPE_OUTBOX;
outbox.synchronize = false;
outbox.after = 0;
outbox.id = folder.insertFolder(outbox);
}
// Build outgoing message
EntityMessage out = new EntityMessage();
out.folder = outbox.id;
out.folder = folder.getOutbox().id;
out.identity = draft.identity;
out.replying = draft.replying;
out.thread = draft.thread;
out.from = draft.from;
out.to = draft.to;
out.cc = draft.cc;
out.bcc = draft.bcc;
out.subject = draft.subject;
out.body = draft.body;
out.received = draft.received;
@@ -557,7 +641,7 @@ public class FragmentCompose extends Fragment {
@Override
public Loader<Throwable> onCreateLoader(int id, Bundle args) {
this.args = args;
PutLoader loader = new PutLoader(getActivity());
PutLoader loader = new PutLoader(getContext());
loader.setArgs(args);
return loader;
}
@@ -574,6 +658,7 @@ public class FragmentCompose extends Fragment {
Toast.makeText(getContext(), send ? R.string.title_queued : R.string.title_draft_saved, Toast.LENGTH_LONG).show();
} else {
Log.w(Helper.TAG, ex + "\n" + Log.getStackTraceString(ex));
bottom_navigation.getMenu().setGroupEnabled(0, true);
Toast.makeText(getContext(), Helper.formatThrowable(ex), Toast.LENGTH_LONG).show();
}
}

View File

@@ -134,7 +134,7 @@ public class FragmentFolder extends Fragment {
@NonNull
@Override
public Loader<Throwable> onCreateLoader(int id, Bundle args) {
PutLoader loader = new PutLoader(getActivity());
PutLoader loader = new PutLoader(getContext());
loader.setArgs(args);
return loader;
}

View File

@@ -33,6 +33,7 @@ import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ProgressBar;
import android.widget.Toast;
import java.util.List;
@@ -86,6 +87,9 @@ public class FragmentFolders extends Fragment {
}
});
// Show hint
Toast.makeText(getContext(), R.string.title_item_edit_hint, Toast.LENGTH_SHORT).show();
return view;
}

View File

@@ -24,6 +24,7 @@ import android.content.Context;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.design.widget.TextInputLayout;
import android.support.v4.app.Fragment;
import android.support.v4.app.LoaderManager;
import android.support.v4.content.AsyncTaskLoader;
@@ -59,11 +60,12 @@ public class FragmentIdentity extends Fragment {
private Spinner spProfile;
private EditText etName;
private EditText etEmail;
private EditText etReplyTo;
private EditText etHost;
private CheckBox cbStartTls;
private EditText etPort;
private EditText etUser;
private EditText etPassword;
private TextInputLayout tilPassword;
private CheckBox cbPrimary;
private CheckBox cbSynchronize;
private Button btnOk;
@@ -86,11 +88,12 @@ public class FragmentIdentity extends Fragment {
spProfile = view.findViewById(R.id.spProvider);
etName = view.findViewById(R.id.etName);
etEmail = view.findViewById(R.id.etEmail);
etReplyTo = view.findViewById(R.id.etReplyTo);
etHost = view.findViewById(R.id.etHost);
cbStartTls = view.findViewById(R.id.cbStartTls);
etPort = view.findViewById(R.id.etPort);
etUser = view.findViewById(R.id.etUser);
etPassword = view.findViewById(R.id.etPassword);
tilPassword = view.findViewById(R.id.tilPassword);
cbPrimary = view.findViewById(R.id.cbPrimary);
cbSynchronize = view.findViewById(R.id.cbSynchronize);
btnOk = view.findViewById(R.id.btnOk);
@@ -152,11 +155,12 @@ public class FragmentIdentity extends Fragment {
args.putLong("id", id);
args.putString("name", etName.getText().toString());
args.putString("email", etEmail.getText().toString());
args.putString("replyto", etReplyTo.getText().toString());
args.putString("host", etHost.getText().toString());
args.putBoolean("starttls", cbStartTls.isChecked());
args.putString("port", etPort.getText().toString());
args.putString("user", etUser.getText().toString());
args.putString("password", etPassword.getText().toString());
args.putString("password", tilPassword.getEditText().getText().toString());
args.putBoolean("primary", cbPrimary.isChecked());
args.putBoolean("synchronize", cbSynchronize.isChecked());
@@ -169,11 +173,12 @@ public class FragmentIdentity extends Fragment {
public void onChanged(@Nullable EntityIdentity identity) {
etName.setText(identity == null ? null : identity.name);
etEmail.setText(identity == null ? null : identity.email);
etReplyTo.setText(identity == null ? null : identity.replyto);
etHost.setText(identity == null ? null : identity.host);
cbStartTls.setChecked(identity == null ? false : identity.starttls);
etPort.setText(identity == null ? null : Long.toString(identity.port));
etUser.setText(identity == null ? null : identity.user);
etPassword.setText(identity == null ? null : identity.password);
tilPassword.getEditText().setText(identity == null ? null : identity.password);
cbPrimary.setChecked(identity == null ? true : identity.primary);
cbSynchronize.setChecked(identity == null ? true : identity.synchronize);
}
@@ -185,7 +190,7 @@ public class FragmentIdentity extends Fragment {
@Override
public void onResume() {
super.onResume();
((AppCompatActivity) getActivity()).getSupportActionBar().setSubtitle(R.string.title_edit_indentity);
((AppCompatActivity) getActivity()).getSupportActionBar().setSubtitle(R.string.title_edit_identity);
}
private static class PutLoader extends AsyncTaskLoader<Throwable> {
@@ -203,9 +208,13 @@ public class FragmentIdentity extends Fragment {
public Throwable loadInBackground() {
try {
long id = args.getLong("id");
String replyto = args.getString("replyto");
String host = args.getString("host");
boolean starttls = args.getBoolean("starttls");
String port = args.getString("port");
if (TextUtils.isEmpty(replyto))
replyto = null;
if (TextUtils.isEmpty(port))
port = "0";
@@ -216,6 +225,7 @@ public class FragmentIdentity extends Fragment {
identity = new EntityIdentity();
identity.name = Objects.requireNonNull(args.getString("name"));
identity.email = Objects.requireNonNull(args.getString("email"));
identity.replyto = replyto;
identity.host = host;
identity.port = Integer.parseInt(port);
identity.starttls = starttls;
@@ -262,7 +272,7 @@ public class FragmentIdentity extends Fragment {
@NonNull
@Override
public Loader<Throwable> onCreateLoader(int id, Bundle args) {
PutLoader loader = new PutLoader(getActivity());
PutLoader loader = new PutLoader(getContext());
loader.setArgs(args);
return loader;
}

View File

@@ -33,10 +33,19 @@ import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentTransaction;
import android.support.v7.app.AlertDialog;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.text.Html;
import android.text.Layout;
import android.text.Spannable;
import android.text.method.LinkMovementMethod;
import android.text.style.URLSpan;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ProgressBar;
@@ -46,20 +55,28 @@ import android.widget.Toast;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class FragmentMessage extends Fragment {
private TextView tvTime;
private TextView tvFrom;
private TextView tvTo;
private TextView tvCc;
private TextView tvBcc;
private RecyclerView rvAttachment;
private TextView tvSubject;
private TextView tvCount;
private BottomNavigationView top_navigation;
private TextView tvBody;
private BottomNavigationView bottom_navigation;
private ProgressBar pbWait;
private Group grpAddress;
private Group grpAttachments;
private Group grpReady;
private AdapterAttachment adapter;
private LiveData<TupleFolderEx> liveFolder;
private ExecutorService executor = Executors.newCachedThreadPool();
@@ -76,7 +93,11 @@ public class FragmentMessage extends Fragment {
final long id = args.getLong("id");
// Get controls
tvFrom = view.findViewById(R.id.tvAddress);
tvFrom = view.findViewById(R.id.tvFrom);
tvTo = view.findViewById(R.id.tvTo);
tvCc = view.findViewById(R.id.tvCc);
tvBcc = view.findViewById(R.id.tvBcc);
rvAttachment = view.findViewById(R.id.rvAttachment);
tvTime = view.findViewById(R.id.tvTime);
tvSubject = view.findViewById(R.id.tvSubject);
tvCount = view.findViewById(R.id.tvCount);
@@ -84,13 +105,45 @@ public class FragmentMessage extends Fragment {
tvBody = view.findViewById(R.id.tvBody);
bottom_navigation = view.findViewById(R.id.bottom_navigation);
pbWait = view.findViewById(R.id.pbWait);
grpAddress = view.findViewById(R.id.grpAddress);
grpAttachments = view.findViewById(R.id.grpAttachments);
grpReady = view.findViewById(R.id.grpReady);
tvTime.setTextIsSelectable(true);
tvFrom.setTextIsSelectable(true);
tvSubject.setTextIsSelectable(true);
tvBody.setTextIsSelectable(true);
tvBody.setMovementMethod(LinkMovementMethod.getInstance());
setHasOptionsMenu(true);
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);
int x = (int) event.getX();
int y = (int) event.getY();
x -= widget.getTotalPaddingLeft();
y -= widget.getTotalPaddingTop();
x += widget.getScrollX();
y += widget.getScrollY();
Layout layout = widget.getLayout();
int line = layout.getLineForVertical(y);
int off = layout.getOffsetForHorizontal(line, x);
URLSpan[] link = buffer.getSpans(off, off, URLSpan.class);
if (link.length != 0) {
Bundle args = new Bundle();
args.putString("link", link[0].getURL());
FragmentWebView fragment = new FragmentWebView();
fragment.setArguments(args);
FragmentTransaction fragmentTransaction = getFragmentManager().beginTransaction();
fragmentTransaction.replace(R.id.content_frame, fragment).addToBackStack("webview");
fragmentTransaction.commit();
}
return true;
}
});
// Wire controls
@@ -140,10 +193,19 @@ public class FragmentMessage extends Fragment {
});
// Initialize
grpAddress.setVisibility(View.GONE);
grpAttachments.setVisibility(View.GONE);
grpReady.setVisibility(View.GONE);
pbWait.setVisibility(View.VISIBLE);
DB db = DB.getInstance(getContext());
rvAttachment.setHasFixedSize(false);
LinearLayoutManager llm = new LinearLayoutManager(getContext());
rvAttachment.setLayoutManager(llm);
adapter = new AdapterAttachment(getContext());
rvAttachment.setAdapter(adapter);
final DB db = DB.getInstance(getContext());
// Observe folder
liveFolder = db.folder().liveFolderEx(folder);
@@ -160,11 +222,13 @@ public class FragmentMessage extends Fragment {
if (FragmentMessage.this.isVisible())
getFragmentManager().popBackStack();
} else {
tvFrom.setText(message.from == null ? null : MessageHelper.getFormattedAddresses(message.from));
tvFrom.setText(MessageHelper.getFormattedAddresses(message.from));
tvTo.setText(MessageHelper.getFormattedAddresses(message.to));
tvCc.setText(MessageHelper.getFormattedAddresses(message.cc));
tvBcc.setText(MessageHelper.getFormattedAddresses(message.bcc));
tvTime.setText(message.sent == null ? null : df.format(new Date(message.sent)));
tvSubject.setText(message.subject);
tvCount.setText(Integer.toString(message.count));
tvCount.setVisibility(message.count > 1 ? View.VISIBLE : View.GONE);
int visibility = (message.ui_seen ? Typeface.NORMAL : Typeface.BOLD);
tvFrom.setTypeface(null, visibility);
@@ -172,12 +236,20 @@ public class FragmentMessage extends Fragment {
tvSubject.setTypeface(null, visibility);
tvCount.setTypeface(null, visibility);
// Observe attachments
db.attachment().liveAttachments(id).removeObservers(FragmentMessage.this);
db.attachment().liveAttachments(id).observe(FragmentMessage.this, attachmentsObserver);
top_navigation.getMenu().findItem(R.id.action_thread).setVisible(message.count > 1);
MenuItem actionSeen = top_navigation.getMenu().findItem(R.id.action_seen);
actionSeen.setIcon(message.ui_seen
? R.drawable.baseline_visibility_off_24
: R.drawable.baseline_visibility_24);
actionSeen.setTitle(message.ui_seen ? R.string.title_unseen : R.string.title_seen);
bottom_navigation.getMenu().findItem(R.id.action_spam).setVisible(message.account != null);
bottom_navigation.getMenu().findItem(R.id.action_archive).setVisible(message.account != null);
tvBody.setText(message.body == null
? null
: Html.fromHtml(HtmlHelper.sanitize(getContext(), message.body, false)));
@@ -200,6 +272,28 @@ public class FragmentMessage extends Fragment {
liveFolder.removeObservers(this);
}
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
inflater.inflate(R.menu.menu_cc, menu);
super.onCreateOptionsMenu(menu, inflater);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.menu_cc:
onMenuCc();
return true;
default:
return super.onOptionsItemSelected(item);
}
}
private void onMenuCc() {
if (grpReady.getVisibility() == View.VISIBLE)
grpAddress.setVisibility(grpAddress.getVisibility() == View.GONE ? View.VISIBLE : View.GONE);
}
Observer<TupleFolderEx> folderObserver = new Observer<TupleFolderEx>() {
@Override
public void onChanged(@Nullable TupleFolderEx folder) {
@@ -209,23 +303,36 @@ public class FragmentMessage extends Fragment {
}
};
Observer<List<EntityAttachment>> attachmentsObserver = new Observer<List<EntityAttachment>>() {
@Override
public void onChanged(@Nullable List<EntityAttachment> attachments) {
adapter.set(attachments);
grpAttachments.setVisibility(attachments.size() > 0 ? View.VISIBLE : View.GONE);
}
};
private void onActionSeen(final long id) {
executor.submit(new Runnable() {
@Override
public void run() {
DB db = DB.getInstance(getContext());
EntityMessage message = db.message().getMessage(id);
message.ui_seen = !message.ui_seen;
db.message().updateMessage(message);
EntityOperation.queue(getContext(), message, EntityOperation.SEEN, message.ui_seen);
try {
DB db = DB.getInstance(getContext());
EntityMessage message = db.message().getMessage(id);
message.ui_seen = !message.ui_seen;
db.message().updateMessage(message);
EntityOperation.queue(getContext(), message, EntityOperation.SEEN, message.ui_seen);
} catch (Throwable ex) {
Log.e(Helper.TAG, ex + "\n" + Log.getStackTraceString(ex));
}
}
});
}
private void onActionThread(long id) {
FragmentMessages fragment = new FragmentMessages();
Bundle args = new Bundle();
args.putLong("thread", id); // message ID
FragmentMessages fragment = new FragmentMessages();
fragment.setArguments(args);
FragmentTransaction fragmentTransaction = getFragmentManager().beginTransaction();
@@ -259,12 +366,16 @@ public class FragmentMessage extends Fragment {
executor.submit(new Runnable() {
@Override
public void run() {
DB db = DB.getInstance(getContext());
EntityMessage message = db.message().getMessage(id);
message.ui_hide = true;
db.message().updateMessage(message);
try {
DB db = DB.getInstance(getContext());
EntityMessage message = db.message().getMessage(id);
message.ui_hide = true;
db.message().updateMessage(message);
EntityOperation.queue(getContext(), message, EntityOperation.DELETE);
EntityOperation.queue(getContext(), message, EntityOperation.DELETE);
} catch (Throwable ex) {
Log.e(Helper.TAG, ex + "\n" + Log.getStackTraceString(ex));
}
}
});
}
@@ -282,18 +393,22 @@ public class FragmentMessage extends Fragment {
executor.submit(new Runnable() {
@Override
public void run() {
DB db = DB.getInstance(getContext());
EntityMessage message = db.message().getMessage(id);
EntityFolder spam = db.folder().getSpamFolder(message.account);
if (spam == null) {
Toast.makeText(getContext(), R.string.title_no_spam, Toast.LENGTH_LONG).show();
return;
try {
DB db = DB.getInstance(getContext());
EntityMessage message = db.message().getMessage(id);
EntityFolder spam = db.folder().getSpamFolder(message.account);
if (spam == null) {
Toast.makeText(getContext(), R.string.title_no_spam, Toast.LENGTH_LONG).show();
return;
}
message.ui_hide = true;
db.message().updateMessage(message);
EntityOperation.queue(getContext(), message, EntityOperation.MOVE, spam.id);
} catch (Throwable ex) {
Log.e(Helper.TAG, ex + "\n" + Log.getStackTraceString(ex));
}
message.ui_hide = true;
db.message().updateMessage(message);
EntityOperation.queue(getContext(), message, EntityOperation.MOVE, spam.id);
}
});
}
@@ -305,18 +420,22 @@ public class FragmentMessage extends Fragment {
executor.submit(new Runnable() {
@Override
public void run() {
DB db = DB.getInstance(getContext());
EntityMessage message = db.message().getMessage(id);
EntityFolder archive = db.folder().getArchiveFolder(message.account);
if (archive == null) {
Toast.makeText(getContext(), R.string.title_no_archive, Toast.LENGTH_LONG).show();
return;
try {
DB db = DB.getInstance(getContext());
EntityMessage message = db.message().getMessage(id);
EntityFolder archive = db.folder().getArchiveFolder(message.account);
if (archive == null) {
Toast.makeText(getContext(), R.string.title_no_archive, Toast.LENGTH_LONG).show();
return;
}
message.ui_hide = true;
db.message().updateMessage(message);
EntityOperation.queue(getContext(), message, EntityOperation.MOVE, archive.id);
} catch (Throwable ex) {
Log.e(Helper.TAG, ex + "\n" + Log.getStackTraceString(ex));
}
message.ui_hide = true;
db.message().updateMessage(message);
EntityOperation.queue(getContext(), message, EntityOperation.MOVE, archive.id);
}
});
}

View File

@@ -21,6 +21,7 @@ package eu.faircode.email;
import android.arch.lifecycle.LiveData;
import android.arch.lifecycle.Observer;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.support.annotation.NonNull;
@@ -28,6 +29,9 @@ import android.support.annotation.Nullable;
import android.support.constraint.Group;
import android.support.design.widget.FloatingActionButton;
import android.support.v4.app.Fragment;
import android.support.v4.app.LoaderManager;
import android.support.v4.content.AsyncTaskLoader;
import android.support.v4.content.Loader;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
@@ -88,6 +92,7 @@ public class FragmentMessages extends Fragment {
tvNoEmail.setVisibility(View.GONE);
grpReady.setVisibility(View.GONE);
pbWait.setVisibility(View.VISIBLE);
fab.setVisibility(View.GONE);
DB db = DB.getInstance(getContext());
@@ -114,6 +119,8 @@ public class FragmentMessages extends Fragment {
((AppCompatActivity) getActivity()).getSupportActionBar().setSubtitle(R.string.title_folder_thread);
else
liveFolder.observe(this, folderObserver);
getLoaderManager().restartLoader(ActivityView.LOADER_MESSAGES_INIT, new Bundle(), initLoaderCallbacks).forceLoad();
}
@Override
@@ -149,4 +156,36 @@ public class FragmentMessages extends Fragment {
}
}
};
private static class InitLoader extends AsyncTaskLoader<Bundle> {
public InitLoader(@NonNull Context context) {
super(context);
}
@Nullable
@Override
public Bundle loadInBackground() {
Bundle result = new Bundle();
EntityFolder drafts = DB.getInstance(getContext()).folder().getPrimaryDraftFolder();
result.putBoolean("drafts", drafts != null);
return result;
}
}
private LoaderManager.LoaderCallbacks initLoaderCallbacks = new LoaderManager.LoaderCallbacks<Bundle>() {
@NonNull
@Override
public Loader<Bundle> onCreateLoader(int id, @Nullable Bundle args) {
return new InitLoader(getContext());
}
@Override
public void onLoadFinished(@NonNull Loader<Bundle> loader, Bundle data) {
fab.setVisibility(data.getBoolean("drafts", false) ? View.VISIBLE : View.GONE);
}
@Override
public void onLoaderReset(@NonNull Loader<Bundle> loader) {
}
};
}

View File

@@ -37,6 +37,8 @@ import android.widget.ProgressBar;
import android.widget.TextView;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class FragmentSetup extends Fragment {
private Button btnAccount;
@@ -48,6 +50,8 @@ public class FragmentSetup extends Fragment {
private TextView tvIdentityDone;
private TextView tvPermissionsDone;
private ExecutorService executor = Executors.newCachedThreadPool();
private static final String[] permissions = new String[]{
Manifest.permission.READ_CONTACTS
};
@@ -86,11 +90,14 @@ public class FragmentSetup extends Fragment {
if (!once) {
once = true;
Bundle args = new Bundle();
if (account != null)
args.putLong("id", account.id);
FragmentAccount fragment = new FragmentAccount();
fragment.setArguments(args);
FragmentTransaction fragmentTransaction = getFragmentManager().beginTransaction();
fragmentTransaction.replace(R.id.content_frame, fragment).addToBackStack("account");
fragmentTransaction.commit();
@@ -120,8 +127,10 @@ public class FragmentSetup extends Fragment {
Bundle args = new Bundle();
if (identity != null)
args.putLong("id", identity.id);
FragmentIdentity fragment = new FragmentIdentity();
fragment.setArguments(args);
FragmentTransaction fragmentTransaction = getFragmentManager().beginTransaction();
fragmentTransaction.replace(R.id.content_frame, fragment).addToBackStack("identity");
fragmentTransaction.commit();
@@ -146,7 +155,7 @@ public class FragmentSetup extends Fragment {
tvIdentityDone.setVisibility(View.INVISIBLE);
tvPermissionsDone.setVisibility(View.INVISIBLE);
DB db = DB.getInstance(getContext());
final DB db = DB.getInstance(getContext());
db.account().liveAccounts(true).observe(this, new Observer<List<EntityAccount>>() {
@Override
@@ -168,6 +177,22 @@ public class FragmentSetup extends Fragment {
onRequestPermissionsResult(0, permissions, grantResults);
// Creat outbox
executor.submit(new Runnable() {
@Override
public void run() {
EntityFolder outbox = db.folder().getOutbox();
if (outbox == null) {
outbox = new EntityFolder();
outbox.name = "OUTBOX";
outbox.type = EntityFolder.TYPE_OUTBOX;
outbox.synchronize = false;
outbox.after = 0;
outbox.id = db.folder().insertFolder(outbox);
}
}
});
return view;
}

View File

@@ -0,0 +1,84 @@
package eu.faircode.email;
/*
This file is part of Safe email.
Safe email 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.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.app.Fragment;
import android.support.v7.app.AppCompatActivity;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.webkit.WebChromeClient;
import android.webkit.WebSettings;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import android.widget.ProgressBar;
// https://developer.android.com/reference/android/webkit/WebView
public class FragmentWebView extends Fragment {
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);
progressBar.setProgress(0);
progressBar.setVisibility(View.VISIBLE);
WebSettings settings = webview.getSettings();
settings.setJavaScriptEnabled(true);
settings.setLoadWithOverviewMode(true);
settings.setUseWideViewPort(true);
//settings.setBuiltInZoomControls(true);
webview.setWebViewClient(new WebViewClient() {
public boolean shouldOverrideUrlLoading(WebView view, String url) {
view.loadUrl(url);
return false;
}
});
webview.setWebChromeClient(new WebChromeClient() {
public void onProgressChanged(WebView view, int progress) {
progressBar.setProgress(progress);
if (progress == 100)
progressBar.setVisibility(View.GONE);
}
});
url = getArguments().getString("link");
webview.loadUrl(url);
return view;
}
@Override
public void onResume() {
super.onResume();
((AppCompatActivity) getActivity()).getSupportActionBar().setSubtitle(url);
}
}

View File

@@ -67,6 +67,14 @@ public class Helper {
return sb.toString();
}
static String humanReadableByteCount(long bytes, boolean si) {
int unit = si ? 1000 : 1024;
if (bytes < unit) return bytes + " B";
int exp = (int) (Math.log(bytes) / Math.log(unit));
String pre = (si ? "kMGTPE" : "KMGTPE").charAt(exp - 1) + (si ? "" : "i");
return String.format("%.1f %sB", bytes / Math.pow(unit, exp), pre);
}
static StringBuilder getDebugInfo() {
StringBuilder sb = new StringBuilder();

View File

@@ -20,6 +20,7 @@ package eu.faircode.email;
*/
import android.content.Context;
import android.text.Html;
import android.text.TextUtils;
import org.jsoup.Jsoup;
@@ -36,7 +37,7 @@ import java.util.List;
public class HtmlHelper implements NodeVisitor {
private Context context;
private String newline;
private List<String> links = new ArrayList<>();
private List<String> refs = new ArrayList<>();
private StringBuilder sb = new StringBuilder();
private HtmlHelper(Context context, boolean reply) {
@@ -61,28 +62,36 @@ public class HtmlHelper implements NodeVisitor {
if (StringUtil.in(name, "br", "dd", "dt", "p", "h1", "h2", "h3", "h4", "h5", "div"))
sb.append(newline);
else if (name.equals("a")) {
String link = node.absUrl("href");
if (!TextUtils.isEmpty(link)) {
if (!links.contains(link))
links.add(link);
sb.append(" ").append(context.getString(R.string.title_link, link, links.size()));
String ref = node.absUrl("href");
if (!TextUtils.isEmpty(ref)) {
if (!refs.contains(ref))
refs.add(ref);
String alt = node.attr("alt");
if (TextUtils.isEmpty(alt))
alt = context.getString(R.string.title_link);
alt = Html.escapeHtml(alt);
sb.append(" ").append(String.format("<a href=\"%s\">%s [%d]</a>", ref, alt, refs.size()));
}
} else if (name.equals("img")) {
String link = node.absUrl("src");
if (!TextUtils.isEmpty(link)) {
if (!links.contains(link))
links.add(link);
sb.append(" ").append(context.getString(R.string.title_image, link, links.size()));
String ref = node.absUrl("src");
if (!TextUtils.isEmpty(ref)) {
if (!refs.contains(ref))
refs.add(ref);
String alt = node.attr("alt");
if (TextUtils.isEmpty(alt))
alt = context.getString(R.string.title_image);
alt = Html.escapeHtml(alt);
sb.append(" ").append(String.format("<a href=\"%s\">%s [%d]</a>", ref, alt, refs.size()));
}
}
}
@Override
public String toString() {
if (links.size() > 0)
if (refs.size() > 0)
sb.append(newline).append(newline);
for (int i = 0; i < links.size(); i++)
sb.append(String.format("[%d] %s ", i + 1, links.get(i))).append(newline);
for (int i = 0; i < refs.size(); i++)
sb.append(String.format("[%d] %s ", i + 1, refs.get(i))).append(newline);
return sb.toString();
}

View File

@@ -37,12 +37,14 @@ import java.util.List;
import java.util.Properties;
import javax.mail.Address;
import javax.mail.BodyPart;
import javax.mail.Flags;
import javax.mail.Message;
import javax.mail.MessagingException;
import javax.mail.Multipart;
import javax.mail.Part;
import javax.mail.Session;
import javax.mail.internet.ContentType;
import javax.mail.internet.InternetAddress;
import javax.mail.internet.MimeMessage;
@@ -90,7 +92,10 @@ public class MessageHelper {
imessage.setRecipients(Message.RecipientType.TO, MessageHelper.decodeAddresses(message.to));
if (message.cc != null)
imessage.setRecipients(Message.RecipientType.CC, MessageHelper.decodeAddresses(message.to));
imessage.setRecipients(Message.RecipientType.CC, MessageHelper.decodeAddresses(message.cc));
if (message.bcc != null)
imessage.setRecipients(Message.RecipientType.BCC, MessageHelper.decodeAddresses(message.bcc));
if (message.subject != null)
imessage.setSubject(message.subject);
@@ -120,6 +125,10 @@ public class MessageHelper {
this.imessage = new MimeMessage(isession, is);
}
boolean getSeen() throws MessagingException {
return imessage.isSet(Flags.Flag.SEEN);
}
String getMessageID() throws MessagingException {
return imessage.getHeader("Message-ID", null);
}
@@ -153,11 +162,17 @@ public class MessageHelper {
return encodeAddresses(imessage.getRecipients(Message.RecipientType.CC));
}
String getBcc() throws MessagingException, JSONException {
return encodeAddresses(imessage.getRecipients(Message.RecipientType.BCC));
}
String getReply() throws MessagingException, JSONException {
return encodeAddresses(imessage.getReplyTo());
}
static String encodeAddresses(Address[] addresses) throws JSONException {
if (addresses == null)
return null;
JSONArray jaddresses = new JSONArray();
if (addresses != null)
for (Address address : addresses)
@@ -175,6 +190,8 @@ public class MessageHelper {
}
static Address[] decodeAddresses(String json) {
if (json == null)
return new Address[0];
List<Address> result = new ArrayList<>();
try {
JSONArray jaddresses = new JSONArray(json);
@@ -194,11 +211,9 @@ public class MessageHelper {
return result.toArray(new Address[0]);
}
String getHtml() throws MessagingException {
return getHtml(imessage);
}
static String getFormattedAddresses(String json) {
if (json == null)
return null;
try {
List<String> addresses = new ArrayList<>();
for (Address address : decodeAddresses(json))
@@ -217,10 +232,14 @@ public class MessageHelper {
}
}
String getHtml() throws MessagingException {
return getHtml(imessage);
}
private String getHtml(Part part) throws MessagingException {
if (part.isMimeType("text/*"))
try {
String s = (String) part.getContent();
String s = part.getContent().toString();
if (part.isMimeType("text/plain"))
s = "<pre>" + s.replaceAll("\\r?\\n", "<br />") + "</pre>";
return s;
@@ -267,8 +286,43 @@ public class MessageHelper {
return null;
}
boolean getSeen() throws MessagingException {
return imessage.isSet(Flags.Flag.SEEN);
public List<EntityAttachment> getAttachments() throws IOException, MessagingException {
List<EntityAttachment> result = new ArrayList<>();
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)));
}
return result;
}
private List<EntityAttachment> getAttachments(BodyPart part) throws IOException, MessagingException {
List<EntityAttachment> result = new ArrayList<>();
Object content = part.getContent();
if (content instanceof InputStream || content instanceof String) {
if (Part.ATTACHMENT.equalsIgnoreCase(part.getDisposition()) || !TextUtils.isEmpty(part.getFileName())) {
ContentType ct = new ContentType(part.getContentType());
EntityAttachment attachment = new EntityAttachment();
attachment.sequence = result.size() + 1;
attachment.name = part.getFileName();
attachment.type = ct.getBaseType();
attachment.part = part;
result.add(attachment);
}
} else if (content instanceof Multipart) {
Multipart multipart = (Multipart) content;
for (int i = 0; i < multipart.getCount(); i++)
result.addAll(getAttachments(multipart.getBodyPart(i)));
}
return result;
}
String getRaw() throws IOException, MessagingException {

View File

@@ -25,8 +25,12 @@ import android.util.Log;
import org.xmlpull.v1.XmlPullParser;
import java.text.Collator;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Locale;
public class Provider {
public String name;
@@ -77,6 +81,16 @@ public class Provider {
} catch (Throwable ex) {
Log.e(Helper.TAG, ex.toString() + "\n" + Log.getStackTraceString(ex));
}
final Collator collator = Collator.getInstance(Locale.getDefault());
collator.setStrength(Collator.SECONDARY); // Case insensitive, process accents etc
Collections.sort(result, new Comparator<Provider>() {
@Override
public int compare(Provider p1, Provider p2) {
return collator.compare(p1.name, p2.name);
}
});
return result;
}

View File

@@ -50,6 +50,9 @@ import com.sun.mail.imap.protocol.IMAPProtocol;
import org.json.JSONArray;
import org.json.JSONException;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
@@ -63,6 +66,7 @@ import javax.mail.FetchProfile;
import javax.mail.Flags;
import javax.mail.Folder;
import javax.mail.FolderClosedException;
import javax.mail.FolderNotFoundException;
import javax.mail.Message;
import javax.mail.MessageRemovedException;
import javax.mail.MessagingException;
@@ -101,7 +105,10 @@ public class ServiceSynchronize extends LifecycleService {
}
public ServiceSynchronize() {
// https://docs.oracle.com/javaee/6/api/javax/mail/internet/package-summary.html
System.setProperty("mail.mime.ignoreunknownencoding", "true");
System.setProperty("mail.mime.decodefilename", "true");
System.setProperty("mail.mime.encodefilename", "true");
}
@Override
@@ -383,7 +390,7 @@ public class ServiceSynchronize extends LifecycleService {
Log.i(Helper.TAG, account.name + " stopped");
}
private void monitorFolder(final EntityAccount account, final EntityFolder folder, final IMAPStore istore) throws MessagingException, JSONException {
private void monitorFolder(final EntityAccount account, final EntityFolder folder, final IMAPStore istore) throws MessagingException, JSONException, IOException {
IMAPFolder ifolder = null;
try {
Log.i(Helper.TAG, folder.name + " start");
@@ -498,7 +505,7 @@ public class ServiceSynchronize extends LifecycleService {
};
// Listen for process operations requests
LocalBroadcastManager lbm = LocalBroadcastManager.getInstance(ServiceSynchronize.this);
LocalBroadcastManager lbm = LocalBroadcastManager.getInstance(this);
lbm.registerReceiver(receiver, new IntentFilter(ACTION_PROCESS_OPERATIONS + folder.id));
Log.i(Helper.TAG, folder.name + " listen process id=" + folder.id);
try {
@@ -545,6 +552,10 @@ public class ServiceSynchronize extends LifecycleService {
lbm.unregisterReceiver(receiver);
Log.i(Helper.TAG, folder.name + " unlisten process id=" + folder.id);
}
} catch (FolderNotFoundException ex) {
Log.w(Helper.TAG, folder.name + " " + ex + "\n" + Log.getStackTraceString(ex));
folder.synchronize = false;
DB.getInstance(this).folder().updateFolder(folder);
} finally {
if (ifolder != null && ifolder.isOpen()) {
try {
@@ -557,156 +568,151 @@ public class ServiceSynchronize extends LifecycleService {
}
}
private void processOperations(EntityFolder folder, IMAPStore istore, IMAPFolder ifolder) throws MessagingException, JSONException {
private void processOperations(EntityFolder folder, IMAPStore istore, IMAPFolder ifolder) throws MessagingException, JSONException, IOException {
try {
Log.i(Helper.TAG, folder.name + " start process");
DB db = DB.getInstance(ServiceSynchronize.this);
DB db = DB.getInstance(this);
DaoOperation operation = db.operation();
DaoMessage message = db.message();
for (TupleOperationEx op : operation.getOperations(folder.id)) {
Log.i(Helper.TAG, folder.name +
" Process op=" + op.id + "/" + op.name +
" args=" + op.args +
" msg=" + op.message);
for (TupleOperationEx op : operation.getOperations(folder.id))
try {
Log.i(Helper.TAG, folder.name +
" start op=" + op.id + "/" + op.name +
" args=" + op.args +
" msg=" + op.message);
JSONArray jargs = new JSONArray(op.args);
if (EntityOperation.SEEN.equals(op.name)) {
// Mark message (un)seen
JSONArray jargs = new JSONArray(op.args);
try {
Message imessage = ifolder.getMessageByUID(op.uid);
if (imessage != null)
imessage.setFlag(Flags.Flag.SEEN, jargs.getBoolean(0));
else
Log.w(Helper.TAG, "Remote message not found uid=" + op.uid);
} catch (MessagingException ex) {
// Countermeasure
Log.i(Helper.TAG, folder.name + " countermeasure " + op.id + "/" + op.name);
EntityMessage msg = message.getMessage(op.message);
msg.ui_seen = msg.seen;
message.updateMessage(msg);
throw ex;
}
} else if (EntityOperation.ADD.equals(op.name)) {
// Append message
try {
EntityMessage msg = message.getMessage(op.message);
Properties props = MessageHelper.getSessionProperties();
Session isession = Session.getDefaultInstance(props, null);
MimeMessage imessage = MessageHelper.from(msg, isession);
ifolder.appendMessages(new Message[]{imessage});
// Draft can be saved multiple times
if (msg.uid != null) {
Message previously = ifolder.getMessageByUID(msg.uid);
previously.setFlag(Flags.Flag.DELETED, true);
ifolder.expunge();
}
message.deleteMessage(op.message);
} catch (MessagingException ex) {
// Countermeasure
// TODO: try again?
throw ex;
}
} else if (EntityOperation.MOVE.equals(op.name)) {
// Move message
try {
Message imessage = ifolder.getMessageByUID(op.uid);
EntityFolder archive = db.folder().getFolder(jargs.getLong(0));
Folder target = istore.getFolder(archive.name);
ifolder.moveMessages(new Message[]{imessage}, target);
message.deleteMessage(op.message);
} catch (MessagingException ex) {
// Countermeasure
Log.i(Helper.TAG, folder.name + " countermeasure " + op.id + "/" + op.name);
EntityMessage msg = message.getMessage(op.message);
msg.ui_hide = false;
message.updateMessage(msg);
throw ex;
}
} else if (EntityOperation.DELETE.equals(op.name)) {
// Delete message
try {
if (op.uid != null) {
if (EntityOperation.SEEN.equals(op.name)) {
// Mark message (un)seen
Message imessage = ifolder.getMessageByUID(op.uid);
if (imessage != null) {
imessage.setFlag(Flags.Flag.DELETED, true);
ifolder.expunge();
} else
Log.w(Helper.TAG, "Remote message not found uid=" + op.uid);
} else {
// Not appended draft
Log.w(Helper.TAG, "Delete without uid id=" + op.message);
}
if (imessage == null)
throw new MessageRemovedException();
imessage.setFlag(Flags.Flag.SEEN, jargs.getBoolean(0));
message.deleteMessage(op.message);
} catch (MessagingException ex) {
// Countermeasure
Log.i(Helper.TAG, folder.name + " countermeasure " + op.id + "/" + op.name);
EntityMessage msg = message.getMessage(op.message);
msg.ui_hide = false;
message.updateMessage(msg);
throw ex;
}
} else if (EntityOperation.ADD.equals(op.name)) {
if (!folder.synchronize) {
// Local drafts
Log.w(Helper.TAG, "Folder synchronization disabled");
return;
}
} else if (EntityOperation.SEND.equals(op.name)) {
// Send message
EntityMessage msg = message.getMessage(op.message);
EntityMessage reply = (msg.replying == null ? null : message.getMessage(msg.replying));
EntityIdentity ident = db.identity().getIdentity(msg.identity);
// Append message
EntityMessage msg = message.getMessage(op.message);
Properties props = MessageHelper.getSessionProperties();
Session isession = Session.getDefaultInstance(props, null);
MimeMessage imessage = MessageHelper.from(msg, isession);
ifolder.appendMessages(new Message[]{imessage});
try {
Properties props = MessageHelper.getSessionProperties();
Session isession = Session.getDefaultInstance(props, null);
// Drafts can be appended multiple times
try {
if (msg.uid != null) {
Message previously = ifolder.getMessageByUID(msg.uid);
previously.setFlag(Flags.Flag.DELETED, true);
ifolder.expunge();
}
} finally {
// Remote will report appended
message.deleteMessage(op.message);
}
MimeMessage imessage;
if (reply == null)
imessage = MessageHelper.from(msg, isession);
else
imessage = MessageHelper.from(msg, reply, isession);
if (ident.replyto != null)
imessage.setReplyTo(new Address[]{new InternetAddress(ident.replyto)});
Transport itransport = isession.getTransport(ident.starttls ? "smtp" : "smtps");
try {
itransport.connect(ident.host, ident.port, ident.user, ident.password);
Address[] to = imessage.getRecipients(Message.RecipientType.TO);
itransport.sendMessage(imessage, to);
Log.i(Helper.TAG, "Sent via " + ident.host + "/" + ident.user +
" to " + TextUtils.join(", ", to));
// Make sure the message is sent only once
operation.deleteOperation(op.id);
} else if (EntityOperation.MOVE.equals(op.name)) {
// Move message
EntityFolder archive = db.folder().getFolder(jargs.getLong(0));
Message imessage = ifolder.getMessageByUID(op.uid);
Folder target = istore.getFolder(archive.name);
ifolder.moveMessages(new Message[]{imessage}, target);
message.deleteMessage(op.message);
} finally {
itransport.close();
}
} catch (MessagingException ex) {
// Countermeasure
Log.i(Helper.TAG, folder.name + " countermeasure " + op.id + "/" + op.name);
EntityFolder drafts = db.folder().getPrimaryDraftFolder();
msg.folder = drafts.id;
message.updateMessage(msg);
// Message will not be sent to remote
throw ex;
} else if (EntityOperation.DELETE.equals(op.name)) {
// Delete message
Message imessage = ifolder.getMessageByUID(op.uid);
if (imessage == null)
throw new MessageRemovedException();
imessage.setFlag(Flags.Flag.DELETED, true);
ifolder.expunge();
message.deleteMessage(op.message);
} else if (EntityOperation.SEND.equals(op.name)) {
// Send message
EntityMessage msg = message.getMessage(op.message);
EntityMessage reply = (msg.replying == null ? null : message.getMessage(msg.replying));
EntityIdentity ident = db.identity().getIdentity(msg.identity);
if (!ident.synchronize) {
// Message will remain in outbox
return;
}
Properties props = MessageHelper.getSessionProperties();
Session isession = Session.getDefaultInstance(props, null);
MimeMessage imessage;
if (reply == null)
imessage = MessageHelper.from(msg, isession);
else
imessage = MessageHelper.from(msg, reply, isession);
if (ident.replyto != null)
imessage.setReplyTo(new Address[]{new InternetAddress(ident.replyto)});
Transport itransport = isession.getTransport(ident.starttls ? "smtp" : "smtps");
try {
itransport.connect(ident.host, ident.port, ident.user, ident.password);
Address[] to = imessage.getAllRecipients();
itransport.sendMessage(imessage, to);
Log.i(Helper.TAG, "Sent via " + ident.host + "/" + ident.user +
" to " + TextUtils.join(", ", to));
// Make sure the message is sent only once
operation.deleteOperation(op.id);
message.deleteMessage(op.message);
} finally {
itransport.close();
}
} else if (EntityOperation.ATTACHMENT.equals(op.name)) {
int sequence = jargs.getInt(0);
EntityAttachment attachment = db.attachment().getAttachment(op.message, sequence);
Message imessage = ifolder.getMessageByUID(op.uid);
if (imessage == null)
throw new MessageRemovedException();
Properties props = MessageHelper.getSessionProperties();
Session isession = Session.getDefaultInstance(props, null);
MessageHelper helper = new MessageHelper((MimeMessage) imessage);
EntityAttachment a = helper.getAttachments().get(sequence - 1);
InputStream is = a.part.getInputStream();
ByteArrayOutputStream os = new ByteArrayOutputStream();
byte[] buffer = new byte[4096];
for (int len = is.read(buffer); len != -1; len = is.read(buffer))
os.write(buffer, 0, len);
attachment.content = os.toByteArray();
db.attachment().updateAttachment(attachment);
Log.i(Helper.TAG, "Downloaded bytes=" + attachment.content.length);
} else
throw new MessagingException("Unknown operation name=" + op.name);
// Operation succeeded
operation.deleteOperation(op.id);
} catch (MessageRemovedException ex) {
Log.w(Helper.TAG, ex + "\n" + Log.getStackTraceString(ex));
// There is no use in repeating
operation.deleteOperation(op.id);
}
} else
throw new MessagingException("Unknown operation name=" + op.name);
operation.deleteOperation(op.id);
}
} finally {
Log.i(Helper.TAG, folder.name + " end op=" + op.id + "/" + op.name);
}
} finally {
Log.i(Helper.TAG, folder.name + " end process");
}
@@ -723,19 +729,19 @@ public class ServiceSynchronize extends LifecycleService {
names.add(folder.name);
Log.i(Helper.TAG, "Local folder count=" + names.size());
Folder[] ifolders = istore.getDefaultFolder().list("*");
Folder[] ifolders = istore.getDefaultFolder().list("*"); // TODO: is the pattern correct?
Log.i(Helper.TAG, "Remote folder count=" + ifolders.length);
for (Folder ifolder : ifolders) {
String[] attrs = ((IMAPFolder) ifolder).getAttributes();
boolean candidate = true;
for (String attr : attrs) {
if ("\\Noselect".equals(attr)) {
if ("\\Noselect".equals(attr)) { // TODO: is this attribute correct?
candidate = false;
break;
}
if (attr.startsWith("\\"))
if (EntityFolder.STANDARD_FOLDER_ATTR.contains(attr.substring(1))) {
if (EntityFolder.SYSTEM_FOLDER_ATTR.contains(attr.substring(1))) {
candidate = false;
break;
}
@@ -765,11 +771,11 @@ public class ServiceSynchronize extends LifecycleService {
}
}
private void synchronizeMessages(EntityFolder folder, IMAPFolder ifolder) throws MessagingException, JSONException {
private void synchronizeMessages(EntityFolder folder, IMAPFolder ifolder) throws MessagingException, JSONException, IOException {
try {
Log.i(Helper.TAG, folder.name + " start sync after=" + folder.after);
DB db = DB.getInstance(ServiceSynchronize.this);
DB db = DB.getInstance(this);
DaoMessage dao = db.message();
// Get reference times
@@ -844,57 +850,77 @@ public class ServiceSynchronize extends LifecycleService {
}
}
private void synchronizeMessage(EntityFolder folder, IMAPFolder ifolder, IMAPMessage imessage) throws MessagingException, JSONException {
private void synchronizeMessage(EntityFolder folder, IMAPFolder ifolder, IMAPMessage imessage) throws MessagingException, JSONException, IOException {
FetchProfile fp = new FetchProfile();
fp.add(UIDFolder.FetchProfileItem.UID);
fp.add(IMAPFolder.FetchProfileItem.FLAGS);
ifolder.fetch(new Message[]{imessage}, fp);
long uid = ifolder.getUID(imessage);
Log.i(Helper.TAG, folder.name + " sync uid=" + uid);
try {
Log.i(Helper.TAG, folder.name + " start sync uid=" + uid);
MessageHelper helper = new MessageHelper(imessage);
boolean seen = helper.getSeen();
if (imessage.isExpunged()) {
Log.i(Helper.TAG, folder.name + " expunged uid=" + uid);
return;
}
if (imessage.isSet(Flags.Flag.DELETED)) {
Log.i(Helper.TAG, folder.name + " deleted uid=" + uid);
return;
}
DB db = DB.getInstance(ServiceSynchronize.this);
EntityMessage message = db.message().getMessage(folder.id, uid);
if (message == null) {
FetchProfile fp1 = new FetchProfile();
fp1.add(FetchProfile.Item.ENVELOPE);
fp1.add(FetchProfile.Item.CONTENT_INFO);
fp1.add(IMAPFolder.FetchProfileItem.HEADERS);
fp1.add(IMAPFolder.FetchProfileItem.MESSAGE);
ifolder.fetch(new Message[]{imessage}, fp1);
MessageHelper helper = new MessageHelper(imessage);
boolean seen = helper.getSeen();
message = new EntityMessage();
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 = null;
message.reply = helper.getReply();
message.subject = imessage.getSubject();
message.body = helper.getHtml();
message.received = imessage.getReceivedDate().getTime();
message.sent = imessage.getSentDate().getTime();
message.seen = seen;
message.ui_seen = seen;
message.ui_hide = false;
DB db = DB.getInstance(this);
EntityMessage message = db.message().getMessage(folder.id, uid);
if (message == null) {
FetchProfile fp1 = new FetchProfile();
fp1.add(FetchProfile.Item.ENVELOPE);
fp1.add(FetchProfile.Item.CONTENT_INFO);
fp1.add(IMAPFolder.FetchProfileItem.HEADERS);
fp1.add(IMAPFolder.FetchProfileItem.MESSAGE);
ifolder.fetch(new Message[]{imessage}, fp1);
message.id = db.message().insertMessage(message);
Log.i(Helper.TAG, folder.name + " added uid=" + uid + " id=" + message.id);
} else if (message.seen != seen) {
message.seen = seen;
message.ui_seen = seen;
message = new EntityMessage();
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.body = helper.getHtml();
message.received = imessage.getReceivedDate().getTime();
message.sent = imessage.getSentDate().getTime();
message.seen = seen;
message.ui_seen = seen;
message.ui_hide = false;
db.message().updateMessage(message);
Log.i(Helper.TAG, folder.name + " updated uid=" + uid + " id=" + message.id);
message.id = db.message().insertMessage(message);
Log.i(Helper.TAG, folder.name + " added id=" + message.id);
for (EntityAttachment attachment : helper.getAttachments()) {
Log.i(Helper.TAG, "attachment name=" + attachment.name + " type=" + attachment.type);
attachment.message = message.id;
db.attachment().insertAttachment(attachment);
}
} else if (message.seen != seen) {
message.seen = seen;
message.ui_seen = seen;
db.message().updateMessage(message);
Log.i(Helper.TAG, folder.name + " updated id=" + message.id);
}
} finally {
Log.i(Helper.TAG, folder.name + " end sync uid=" + uid);
}
}

View File

@@ -24,6 +24,7 @@ public class TupleMessageEx extends EntityMessage {
public String folderType;
public int count;
public int unseen;
public int attachments;
@Override
public boolean equals(Object obj) {
@@ -32,7 +33,8 @@ public class TupleMessageEx extends EntityMessage {
return (super.equals(obj) &&
this.folderType.equals(other.folderType) &&
this.count == other.count &&
this.unseen == other.unseen);
this.unseen == other.unseen &&
this.attachments == other.attachments);
}
return super.equals(obj);
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 264 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 302 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 408 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 516 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 274 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 298 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 397 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 509 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 187 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 164 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 207 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 185 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 190 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 167 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 211 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 186 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 247 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 260 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 355 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 430 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 249 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 260 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 353 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 425 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 166 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 205 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 302 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 389 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 171 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 209 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 298 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 374 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 143 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 121 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 164 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 159 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 146 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 121 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 167 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 161 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 156 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 184 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 260 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 317 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 159 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 187 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 260 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 313 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 302 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 389 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 516 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 718 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 298 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 374 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 509 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 710 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 164 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 159 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 185 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 208 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 167 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 161 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 186 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 208 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 260 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 317 B

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