mirror of
https://github.com/M66B/FairEmail.git
synced 2026-03-26 02:45:31 +01:00
Cloud sync: subset of account/identity properties
This commit is contained in:
@@ -45,8 +45,10 @@ import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.SecretKey;
|
||||
@@ -127,6 +129,8 @@ public class CloudSync {
|
||||
receiveRemoteData(context, user, password, lrevision, jstatus);
|
||||
} else
|
||||
receiveRemoteData(context, user, password, lrevision, jstatus);
|
||||
else if (BuildConfig.DEBUG)
|
||||
receiveRemoteData(context, user, password, lrevision - 1, jstatus);
|
||||
} else
|
||||
throw new IllegalArgumentException("Expected one status item");
|
||||
|
||||
@@ -213,13 +217,13 @@ public class CloudSync {
|
||||
|
||||
JSONObject jidentity = new JSONObject();
|
||||
jidentity.put("key", "identity." + identity.uuid);
|
||||
jidentity.put("val", identity.toJSON().toString());
|
||||
jidentity.put("val", toJSON(identity).toString());
|
||||
jidentity.put("rev", lrevision);
|
||||
jupload.put(jidentity);
|
||||
}
|
||||
|
||||
JSONObject jaccountdata = new JSONObject();
|
||||
jaccountdata.put("account", account.toJSON());
|
||||
jaccountdata.put("account", toJSON(account));
|
||||
jaccountdata.put("identities", jidentitieuuids);
|
||||
|
||||
JSONObject jaccount = new JSONObject();
|
||||
@@ -254,8 +258,13 @@ public class CloudSync {
|
||||
throws JSONException, GeneralSecurityException, IOException {
|
||||
DB db = DB.getInstance(context);
|
||||
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
|
||||
boolean sync_accounts = prefs.getBoolean("cloud_sync_accounts", true);
|
||||
boolean sync_accounts_delete = prefs.getBoolean("cloud_sync_accounts_delete", false);
|
||||
boolean sync_blocked_senders = prefs.getBoolean("cloud_sync_blocked_senders", true);
|
||||
boolean sync_filter_rules = prefs.getBoolean("cloud_sync_filter_rules", true);
|
||||
|
||||
// New revision
|
||||
boolean updates = false;
|
||||
JSONArray jdownload = new JSONArray();
|
||||
|
||||
// Get accounts
|
||||
@@ -286,11 +295,12 @@ public class CloudSync {
|
||||
long revision = jaccount.getLong("rev");
|
||||
|
||||
JSONObject jaccountdata = new JSONObject(value);
|
||||
EntityAccount raccount = EntityAccount.fromJSON(jaccountdata.getJSONObject("account"));
|
||||
EntityAccount raccount = accountFromJSON(jaccountdata.getJSONObject("account"));
|
||||
EntityAccount laccount = db.account().getAccountByUUID(raccount.uuid);
|
||||
|
||||
JSONArray jidentities = jaccountdata.getJSONArray("identities");
|
||||
Log.i("Cloud account " + raccount.uuid + "=" + (laccount == null ? "insert" : "update") +
|
||||
Log.i("Cloud account " + raccount.uuid + "=" +
|
||||
(laccount == null ? "insert" : (areEqual(toJSON(raccount), toJSON(laccount)) ? "equal" : "update")) +
|
||||
" rev=" + revision +
|
||||
" identities=" + jidentities +
|
||||
" size=" + value.length());
|
||||
@@ -316,9 +326,11 @@ public class CloudSync {
|
||||
JSONObject jidentity = jitems.getJSONObject(i);
|
||||
String value = jidentity.getString("val");
|
||||
long revision = jidentity.getLong("rev");
|
||||
EntityIdentity ridentity = EntityIdentity.fromJSON(new JSONObject(value));
|
||||
EntityIdentity ridentity = identityFromJSON(new JSONObject(value));
|
||||
EntityIdentity lidentity = db.identity().getIdentityByUUID(ridentity.uuid);
|
||||
Log.i("Cloud identity " + ridentity.uuid + "=" + (lidentity == null ? "insert" : "update") +
|
||||
|
||||
Log.i("Cloud identity " + ridentity.uuid + "=" +
|
||||
(lidentity == null ? "insert" : (areEqual(toJSON(ridentity), toJSON(lidentity)) ? "equal" : "update")) +
|
||||
" rev=" + revision +
|
||||
" size=" + value.length());
|
||||
}
|
||||
@@ -326,6 +338,216 @@ public class CloudSync {
|
||||
}
|
||||
|
||||
prefs.edit().putLong("sync_status", lrevision).apply();
|
||||
|
||||
if (updates)
|
||||
ServiceSynchronize.reload(context, null, true, "sync");
|
||||
}
|
||||
|
||||
private static boolean areEqual(JSONObject o1, JSONObject o2) throws JSONException {
|
||||
if (o1 == null && o2 == null)
|
||||
return true;
|
||||
if (o1 == null || o2 == null)
|
||||
return false;
|
||||
|
||||
Iterator<String> i1 = o1.keys();
|
||||
while (i1.hasNext()) {
|
||||
String k1 = i1.next();
|
||||
if (!o2.has(k1))
|
||||
return false;
|
||||
}
|
||||
|
||||
Iterator<String> i2 = o2.keys();
|
||||
while (i2.hasNext()) {
|
||||
String k2 = i2.next();
|
||||
if (!o2.has(k2))
|
||||
return false;
|
||||
if (!Objects.equals(o1.get(k2), o2.get(k2)))
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static JSONObject toJSON(EntityAccount account) throws JSONException {
|
||||
JSONObject json = new JSONObject();
|
||||
if (account == null)
|
||||
return json;
|
||||
//json.put("id", id);
|
||||
json.put("uuid", account.uuid);
|
||||
//json.put("order", order);
|
||||
json.put("protocol", account.protocol);
|
||||
json.put("host", account.host);
|
||||
json.put("encryption", account.encryption);
|
||||
json.put("insecure", account.insecure);
|
||||
json.put("port", account.port);
|
||||
json.put("auth_type", account.auth_type);
|
||||
json.put("provider", account.provider);
|
||||
json.put("user", account.user);
|
||||
json.put("password", account.password);
|
||||
//json.put("certificate_alias", certificate_alias);
|
||||
json.put("realm", account.realm);
|
||||
json.put("fingerprint", account.fingerprint);
|
||||
|
||||
//json.put("name", name);
|
||||
//json.put("category", category);
|
||||
//json.put("color", color);
|
||||
//json.put("calendar", calendar);
|
||||
|
||||
//json.put("synchronize", synchronize);
|
||||
//json.put("ondemand", ondemand);
|
||||
//json.put("poll_exempted", poll_exempted);
|
||||
//json.put("primary", primary);
|
||||
//json.put("notify", notify);
|
||||
//json.put("browse", browse);
|
||||
//json.put("leave_on_server", leave_on_server);
|
||||
//json.put("leave_deleted", leave_deleted);
|
||||
//json.put("leave_on_device", leave_on_device);
|
||||
//json.put("max_messages", max_messages);
|
||||
//json.put("auto_seen", auto_seen);
|
||||
// not separator
|
||||
|
||||
//json.put("swipe_left", swipe_left);
|
||||
//json.put("swipe_right", swipe_right);
|
||||
|
||||
//json.put("move_to", move_to);
|
||||
|
||||
json.put("poll_interval", account.poll_interval);
|
||||
json.put("keep_alive_noop", account.keep_alive_noop);
|
||||
json.put("partial_fetch", account.partial_fetch);
|
||||
json.put("ignore_size", account.ignore_size);
|
||||
json.put("use_date", account.use_date);
|
||||
json.put("use_received", account.use_received);
|
||||
json.put("unicode", account.unicode);
|
||||
//json.put("conditions", conditions);
|
||||
// not prefix
|
||||
// not created
|
||||
// not tbd
|
||||
// not state
|
||||
// not warning
|
||||
// not error
|
||||
// not last connected
|
||||
return json;
|
||||
}
|
||||
|
||||
public static EntityAccount accountFromJSON(JSONObject json) throws JSONException {
|
||||
EntityAccount account = new EntityAccount();
|
||||
account.uuid = json.getString("uuid");
|
||||
if (json.has("protocol"))
|
||||
account.protocol = json.getInt("protocol");
|
||||
|
||||
account.host = json.getString("host");
|
||||
account.encryption = json.getInt("encryption");
|
||||
account.insecure = (json.has("insecure") && json.getBoolean("insecure"));
|
||||
account.port = json.getInt("port");
|
||||
account.auth_type = json.getInt("auth_type");
|
||||
if (json.has("provider") && !json.isNull("provider"))
|
||||
account.provider = json.getString("provider");
|
||||
account.user = json.getString("user");
|
||||
account.password = json.getString("password");
|
||||
if (json.has("realm") && !json.isNull("realm"))
|
||||
account.realm = json.getString("realm");
|
||||
if (json.has("fingerprint") && !json.isNull("fingerprint"))
|
||||
account.fingerprint = json.getString("fingerprint");
|
||||
|
||||
account.poll_interval = json.getInt("poll_interval");
|
||||
account.keep_alive_noop = json.optBoolean("keep_alive_noop");
|
||||
|
||||
account.partial_fetch = json.optBoolean("partial_fetch", true);
|
||||
account.ignore_size = json.optBoolean("ignore_size", false);
|
||||
account.use_date = json.optBoolean("use_date", false);
|
||||
account.use_received = json.optBoolean("use_received", false);
|
||||
account.unicode = json.optBoolean("unicode", false);
|
||||
|
||||
return account;
|
||||
}
|
||||
|
||||
private static JSONObject toJSON(EntityIdentity identity) throws JSONException {
|
||||
JSONObject json = new JSONObject();
|
||||
if (identity == null)
|
||||
return json;
|
||||
//json.put("id", id);
|
||||
json.put("uuid", identity.uuid);
|
||||
//json.put("name", name);
|
||||
json.put("email", identity.email);
|
||||
// not account
|
||||
//json.put("display", display);
|
||||
//if (color != null)
|
||||
// json.put("color", color);
|
||||
//TODO json.put("signature", signature);
|
||||
|
||||
json.put("host", identity.host);
|
||||
json.put("encryption", identity.encryption);
|
||||
json.put("insecure", identity.insecure);
|
||||
json.put("port", identity.port);
|
||||
json.put("auth_type", identity.auth_type);
|
||||
json.put("provider", identity.provider);
|
||||
json.put("user", identity.user);
|
||||
json.put("password", identity.password);
|
||||
//json.put("certificate_alias", certificate_alias);
|
||||
json.put("realm", identity.realm);
|
||||
json.put("fingerprint", identity.fingerprint);
|
||||
json.put("use_ip", identity.use_ip);
|
||||
json.put("ehlo", identity.ehlo);
|
||||
|
||||
//json.put("synchronize", synchronize);
|
||||
//json.put("primary", primary);
|
||||
//TODO json.put("self", self);
|
||||
//TODO json.put("sender_extra", sender_extra);
|
||||
//TODO json.put("sender_extra_name", sender_extra_name);
|
||||
//TODO json.put("sender_extra_regex", sender_extra_regex);
|
||||
|
||||
//json.put("replyto", replyto);
|
||||
//json.put("cc", cc);
|
||||
//json.put("bcc", bcc);
|
||||
//json.put("internal", internal);
|
||||
|
||||
json.put("unicode", identity.unicode);
|
||||
json.put("octetmime", identity.octetmime);
|
||||
// not plain_only
|
||||
//json.put("sign_default", sign_default);
|
||||
//json.put("encrypt_default", encrypt_default);
|
||||
// not encrypt
|
||||
// delivery_receipt
|
||||
// read_receipt
|
||||
// not store_sent
|
||||
// not sent_folder
|
||||
// not sign_key
|
||||
// sign_key_alias
|
||||
// not tbd
|
||||
// not state
|
||||
// not error
|
||||
// not last_connected
|
||||
// not max_size
|
||||
return json;
|
||||
}
|
||||
|
||||
public static EntityIdentity identityFromJSON(JSONObject json) throws JSONException {
|
||||
EntityIdentity identity = new EntityIdentity();
|
||||
identity.uuid = json.getString("uuid");
|
||||
identity.email = json.getString("email");
|
||||
|
||||
identity.host = json.getString("host");
|
||||
identity.encryption = json.getInt("encryption");
|
||||
identity.insecure = json.optBoolean("insecure");
|
||||
identity.port = json.getInt("port");
|
||||
identity.auth_type = json.getInt("auth_type");
|
||||
if (json.has("provider") && !json.isNull("provider"))
|
||||
identity.provider = json.getString("provider");
|
||||
identity.user = json.getString("user");
|
||||
identity.password = json.getString("password");
|
||||
if (json.has("realm") && !json.isNull("realm"))
|
||||
identity.realm = json.getString("realm");
|
||||
if (json.has("fingerprint") && !json.isNull("fingerprint"))
|
||||
identity.fingerprint = json.getString("fingerprint");
|
||||
if (json.has("use_ip"))
|
||||
identity.use_ip = json.getBoolean("use_ip");
|
||||
if (json.has("ehlo") && !json.isNull("ehlo"))
|
||||
identity.ehlo = json.getString("ehlo");
|
||||
|
||||
identity.unicode = json.optBoolean("unicode");
|
||||
identity.octetmime = json.optBoolean("octetmime");
|
||||
|
||||
return identity;
|
||||
}
|
||||
|
||||
// Lower level
|
||||
|
||||
@@ -564,6 +564,7 @@ public abstract class DB extends RoomDatabase {
|
||||
db.execSQL("CREATE TRIGGER IF NOT EXISTS account_update" +
|
||||
" AFTER UPDATE" +
|
||||
" OF host, encryption, insecure, port, realm, fingerprint" +
|
||||
", poll_interval, keep_alive_noop, partial_fetch, ignore_size, use_date, use_received, unicode" +
|
||||
" ON account" +
|
||||
" BEGIN" +
|
||||
" INSERT INTO sync ('entity', 'reference', 'action', 'time')" +
|
||||
@@ -572,7 +573,7 @@ public abstract class DB extends RoomDatabase {
|
||||
|
||||
db.execSQL("CREATE TRIGGER IF NOT EXISTS account_auth" +
|
||||
" AFTER UPDATE" +
|
||||
" OF auth_type, provider, `user`, password, certificate_alias" +
|
||||
" OF auth_type, provider, `user`, password" +
|
||||
" ON account" +
|
||||
" BEGIN" +
|
||||
" INSERT INTO sync ('entity', 'reference', 'action', 'time')" +
|
||||
@@ -595,7 +596,8 @@ public abstract class DB extends RoomDatabase {
|
||||
|
||||
db.execSQL("CREATE TRIGGER IF NOT EXISTS identity_update" +
|
||||
" AFTER UPDATE" +
|
||||
" OF host, encryption, insecure, port, realm, fingerprint" +
|
||||
" OF email, host, encryption, insecure, port, realm, fingerprint" +
|
||||
", use_ip, ehlo, unicode, octetmime" +
|
||||
" ON identity" +
|
||||
" BEGIN" +
|
||||
" INSERT INTO sync ('entity', 'reference', 'action', 'time')" +
|
||||
@@ -604,7 +606,7 @@ public abstract class DB extends RoomDatabase {
|
||||
|
||||
db.execSQL("CREATE TRIGGER IF NOT EXISTS identity_auth" +
|
||||
" AFTER UPDATE" +
|
||||
" OF auth_type, provider, `user`, password, certificate_alias" +
|
||||
" OF auth_type, provider, `user`, password" +
|
||||
" ON identity" +
|
||||
" BEGIN" +
|
||||
" INSERT INTO sync ('entity', 'reference', 'action', 'time')" +
|
||||
|
||||
@@ -119,6 +119,7 @@ public class FragmentOptionsBackup extends FragmentBase implements SharedPrefere
|
||||
private Button btnLogin;
|
||||
private TextView tvLogin;
|
||||
private CheckBox cbAccounts;
|
||||
private CheckBox cbAccountsDelete;
|
||||
private CheckBox cbBlockedSenders;
|
||||
private CheckBox cbFilterRules;
|
||||
private ImageButton ibSync;
|
||||
@@ -162,6 +163,7 @@ public class FragmentOptionsBackup extends FragmentBase implements SharedPrefere
|
||||
btnLogin = view.findViewById(R.id.btnLogin);
|
||||
tvLogin = view.findViewById(R.id.tvLogin);
|
||||
cbAccounts = view.findViewById(R.id.cbAccounts);
|
||||
cbAccountsDelete = view.findViewById(R.id.cbAccountsDelete);
|
||||
cbBlockedSenders = view.findViewById(R.id.cbBlockedSenders);
|
||||
cbFilterRules = view.findViewById(R.id.cbFilterRules);
|
||||
ibSync = view.findViewById(R.id.ibSync);
|
||||
@@ -214,6 +216,14 @@ public class FragmentOptionsBackup extends FragmentBase implements SharedPrefere
|
||||
@Override
|
||||
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
|
||||
prefs.edit().putBoolean("cloud_sync_accounts", isChecked).apply();
|
||||
cbAccountsDelete.setEnabled(isChecked);
|
||||
}
|
||||
});
|
||||
|
||||
cbAccountsDelete.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
|
||||
@Override
|
||||
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
|
||||
prefs.edit().putBoolean("cloud_sync_accounts_delete", isChecked).apply();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -257,6 +267,8 @@ public class FragmentOptionsBackup extends FragmentBase implements SharedPrefere
|
||||
|
||||
prefs.registerOnSharedPreferenceChangeListener(this);
|
||||
cbAccounts.setChecked(prefs.getBoolean("cloud_sync_accounts", true));
|
||||
cbAccountsDelete.setChecked(prefs.getBoolean("cloud_sync_accounts_delete", false));
|
||||
cbAccountsDelete.setEnabled(cbAccounts.isChecked());
|
||||
cbBlockedSenders.setChecked(prefs.getBoolean("cloud_sync_blocked_senders", true));
|
||||
cbFilterRules.setChecked(prefs.getBoolean("cloud_sync_filter_rules", true));
|
||||
onSharedPreferenceChanged(prefs, null);
|
||||
|
||||
@@ -205,6 +205,18 @@
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/tvCloudInfo" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvCloudExperimental"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="12dp"
|
||||
android:text="@string/title_experimental"
|
||||
android:textAppearance="@style/TextAppearance.AppCompat.Medium"
|
||||
android:textColor="?attr/colorWarning"
|
||||
android:textStyle="bold"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/tvCloudPro" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvUser"
|
||||
android:layout_width="wrap_content"
|
||||
@@ -214,7 +226,7 @@
|
||||
android:text="@string/title_user"
|
||||
android:textAppearance="@style/TextAppearance.AppCompat.Small"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/tvCloudPro" />
|
||||
app:layout_constraintTop_toBottomOf="@id/tvCloudExperimental" />
|
||||
|
||||
<eu.faircode.email.EditTextPlain
|
||||
android:id="@+id/etUser"
|
||||
@@ -300,6 +312,29 @@
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/tvLogin" />
|
||||
|
||||
<CheckBox
|
||||
android:id="@+id/cbAccountsDelete"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="24dp"
|
||||
android:layout_marginTop="12dp"
|
||||
android:text="@string/title_advanced_cloud_accounts_delete"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/cbAccounts" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvAccountsInfo"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="12dp"
|
||||
android:layout_marginEnd="12dp"
|
||||
android:text="@string/title_advanced_cloud_accounts_info"
|
||||
android:textAppearance="@style/TextAppearance.AppCompat.Small"
|
||||
android:textStyle="italic"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/cbAccountsDelete" />
|
||||
|
||||
<CheckBox
|
||||
android:id="@+id/cbBlockedSenders"
|
||||
android:layout_width="wrap_content"
|
||||
@@ -307,7 +342,7 @@
|
||||
android:layout_marginTop="12dp"
|
||||
android:text="@string/title_advanced_cloud_blocked_senders"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/cbAccounts" />
|
||||
app:layout_constraintTop_toBottomOf="@id/tvAccountsInfo" />
|
||||
|
||||
<CheckBox
|
||||
android:id="@+id/cbFilterRules"
|
||||
|
||||
@@ -973,11 +973,14 @@
|
||||
<string name="title_advanced_cleanup_hint">This will delete all temporary files</string>
|
||||
<string name="title_advanced_keywords_hint" translatable="false">Space separated</string>
|
||||
|
||||
<string name="title_experimental" translatable="false">This is an experimental feature!</string>
|
||||
<string name="title_advanced_cloud_security" translatable="false">All data is encrypted end-to-end and the cloud server will never see the username, password and data</string>
|
||||
<string name="title_advanced_cloud_login" translatable="false">Login</string>
|
||||
<string name="title_advanced_cloud_register" translatable="false">Logging in for the first time will automatically create an account</string>
|
||||
<string name="title_advanced_cloud_invalid" translatable="false">Invalid username or password</string>
|
||||
<string name="title_advanced_cloud_accounts" translatable="false">Sync accounts</string>
|
||||
<string name="title_advanced_cloud_accounts_delete" translatable="false">Delete accounts</string>
|
||||
<string name="title_advanced_cloud_accounts_info" translatable="false">Only the server configuration will be synchronized</string>
|
||||
<string name="title_advanced_cloud_blocked_senders" translatable="false">Sync blocked senders</string>
|
||||
<string name="title_advanced_cloud_filter_rules" translatable="false">Sync filter rules</string>
|
||||
<string name="title_advanced_cloud_last_sync" translatable="false">Last sync: %1$s</string>
|
||||
|
||||
Reference in New Issue
Block a user